Skip to content

feat: improve error printing#517

Open
m4tx wants to merge 1 commit intomasterfrom
improve-errors
Open

feat: improve error printing#517
m4tx wants to merge 1 commit intomasterfrom
improve-errors

Conversation

@m4tx
Copy link
Member

@m4tx m4tx commented Mar 22, 2026

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

The change turns this:
before

into this:
after

Copilot AI review requested due to automatic review settings March 22, 2026 15:27
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
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Debug formatting to print the error via Display plus a “caused by” source chain.
  • Preserve the prior Debug output behind alternate formatting ({:#?}).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +180 to +188
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}")?;

Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines +182 to +189
writeln!(f)?;
writeln!(f)?;
writeln!(f, "caused by:")?;
let mut source = self.source();
while let Some(e) = source {
writeln!(f, " {e}")?;

source = e.source();
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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();
}

Copilot uses AI. Check for mistakes.
Comment on lines 172 to +193
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(())
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

github-actions bot commented Mar 22, 2026

🐰 Bencher Report

Branchimprove-errors
Testbedgithub-ubuntu-latest
Click to view all benchmark results
BenchmarkLatencyBenchmark 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%)
🐰 View full continuous benchmarking report in Bencher

Comment on lines +182 to +184
writeln!(f)?;
writeln!(f)?;
writeln!(f, "caused by:")?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that should be within the while loop to show the chain of causality

Copy link
Member Author

@m4tx m4tx Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

codecov bot commented Mar 22, 2026

Codecov Report

❌ Patch coverage is 54.54545% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cot-core/src/error/error_impl.rs 54.54% 0 Missing and 5 partials ⚠️
Flag Coverage Δ
rust 89.97% <54.54%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
cot-core/src/error/error_impl.rs 93.42% <54.54%> (-3.06%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants