Conversation
When using `Result::expect()`, (e.g. in the default `cot::main`) errors are printed using `Debug` printer. This causes very ugly errors to be produced. Instead, when the alternate mode is not being used, just format the error nicely, along with the error source chain. This roughly duplicates the `anyhow::Error` error printing logic: https://docs.rs/anyhow/latest/anyhow/struct.Error.html#method.chain
There was a problem hiding this comment.
Pull request overview
Updates cot-core::Error’s Debug formatting so that {:?} produces a human-friendly error message plus its source chain (similar to anyhow), while {:#?} retains the original structured debug output. This improves the default panic output from Result::unwrap() / expect() in cot::main.
Changes:
- Implement non-alternate
Debugformatting to print the error viaDisplayplus a “caused by” source chain. - Preserve the prior
Debugoutput behind alternate formatting ({:#?}).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Display::fmt(&self.repr.inner, f)?; | ||
|
|
||
| writeln!(f)?; | ||
| writeln!(f)?; | ||
| writeln!(f, "caused by:")?; | ||
| let mut source = self.source(); | ||
| while let Some(e) = source { | ||
| writeln!(f, " {e}")?; | ||
|
|
There was a problem hiding this comment.
The source-chain printing here will duplicate the top-level message for errors created via Error::with_status/Error::internal: the outer Error displays the inner Error via WithStatusCode, and then self.source() returns that same inner Error as the first source, so the first "caused by" entry repeats the root error. Consider printing from self.inner() (both the root message and the chain), or otherwise skipping the first source when it’s the status-propagation Error wrapper.
| Display::fmt(&self.repr.inner, f)?; | |
| writeln!(f)?; | |
| writeln!(f)?; | |
| writeln!(f, "caused by:")?; | |
| let mut source = self.source(); | |
| while let Some(e) = source { | |
| writeln!(f, " {e}")?; | |
| // Print the root error message. | |
| Display::fmt(&self.repr.inner, f)?; | |
| // Prepare to print the source chain. | |
| let root_msg = format!("{}", &self.repr.inner); | |
| writeln!(f)?; | |
| writeln!(f)?; | |
| writeln!(f, "caused by:")?; | |
| let mut source = self.source(); | |
| let mut is_first = true; | |
| while let Some(e) = source { | |
| let msg = format!("{}", e); | |
| // For status-propagation wrappers, the first source may have the | |
| // same message as the root error; skip it to avoid duplication. | |
| if !is_first || msg != root_msg { | |
| writeln!(f, " {msg}")?; | |
| } | |
| is_first = false; |
| writeln!(f)?; | ||
| writeln!(f)?; | ||
| writeln!(f, "caused by:")?; | ||
| let mut source = self.source(); | ||
| while let Some(e) = source { | ||
| writeln!(f, " {e}")?; | ||
|
|
||
| source = e.source(); |
There was a problem hiding this comment.
This always prints a blank-line separator and a "caused by:" section even when self.source() is None, which will produce output ending with an empty "caused by:" block. Consider only emitting the extra newlines and the "caused by" header when at least one source exists.
| writeln!(f)?; | |
| writeln!(f)?; | |
| writeln!(f, "caused by:")?; | |
| let mut source = self.source(); | |
| while let Some(e) = source { | |
| writeln!(f, " {e}")?; | |
| source = e.source(); | |
| let mut source = self.source(); | |
| if source.is_some() { | |
| writeln!(f)?; | |
| writeln!(f)?; | |
| writeln!(f, "caused by:")?; | |
| while let Some(e) = source { | |
| writeln!(f, " {e}")?; | |
| source = e.source(); | |
| } |
| impl Debug for Error { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| Debug::fmt(&self.repr, f) | ||
| // If the alternate (`{:#?}`) formatting has been specified, print out the | ||
| // `Debug` formatting normally. If not, (which is the case when using | ||
| // `Result::unwrap()` or `Result::expect()`) pretty print the error. | ||
| if f.alternate() { | ||
| Debug::fmt(&self.repr, f) | ||
| } else { | ||
| Display::fmt(&self.repr.inner, f)?; | ||
|
|
||
| writeln!(f)?; | ||
| writeln!(f)?; | ||
| writeln!(f, "caused by:")?; | ||
| let mut source = self.source(); | ||
| while let Some(e) = source { | ||
| writeln!(f, " {e}")?; | ||
|
|
||
| source = e.source(); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
The pretty-printing logic in Debug::fmt is new user-facing behavior but there are no tests asserting the {:#?} vs {:?} formatting, the presence/absence of the "caused by" block, and the formatting of multi-level source chains. Adding unit tests in this file’s tests module would help prevent regressions (especially around Error::internal/Error::with_status).
|
| Branch | improve-errors |
| Testbed | github-ubuntu-latest |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result microseconds (µs) (Result Δ%) | Upper Boundary microseconds (µs) (Limit %) |
|---|---|---|---|
| empty_router/empty_router | 📈 view plot 🚷 view threshold | 6,532.60 µs(+9.28%)Baseline: 5,977.82 µs | 7,170.38 µs (91.11%) |
| json_api/json_api | 📈 view plot 🚷 view threshold | 1,089.80 µs(+5.49%)Baseline: 1,033.12 µs | 1,191.23 µs (91.49%) |
| nested_routers/nested_routers | 📈 view plot 🚷 view threshold | 991.90 µs(+4.15%)Baseline: 952.34 µs | 1,092.51 µs (90.79%) |
| single_root_route/single_root_route | 📈 view plot 🚷 view threshold | 964.80 µs(+5.60%)Baseline: 913.60 µs | 1,051.21 µs (91.78%) |
| single_root_route_burst/single_root_route_burst | 📈 view plot 🚷 view threshold | 16,516.00 µs(-6.22%)Baseline: 17,610.86 µs | 21,143.00 µs (78.12%) |
| writeln!(f)?; | ||
| writeln!(f)?; | ||
| writeln!(f, "caused by:")?; |
There was a problem hiding this comment.
I think that should be within the while loop to show the chain of causality
There was a problem hiding this comment.
The Rust stack traces are printed with 0:, 1:, 2:, etc. at the beginning of each frame. Maybe that would do? I don't think I'd like to inflate the error messages with excessive newlines.
Codecov Report❌ Patch coverage is
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
When using
Result::expect(), (e.g. in the defaultcot::main) errors are printed usingDebugprinter. This causes very ugly errors to be produced. Instead, when the alternate mode is not being used, just format the error nicely, along with the error source chain.This roughly duplicates the
anyhow::Errorerror printing logic: https://docs.rs/anyhow/latest/anyhow/struct.Error.html#method.chainThe change turns this:

into this:
