3.4Print debugging: eprintln! and dbg!

Last updated June 11, 2026

The oldest debugging technique is making the program narrate itself: print the values, print the progress, read the story. It's unglamorous, it works everywhere, and Rust ships a macro that does it unusually well. But first, a distinction that makes all debug output better.

Two output streams

Your program actually has two output channels. Standard output (stdout) is where println! writes: the program's real product. Standard error (stderr) is a second stream for status and complaints, and eprintln! (note the e) writes there, with identical syntax.

On a plain terminal they look interleaved, so the difference seems cosmetic. It stops being cosmetic the moment output gets redirected. cargo run > results.txt sends stdout into the file; stderr still appears on screen. Debug messages on stdout would silently contaminate results.txt; debug messages on stderr stay visible and keep the product clean. (You've already seen the principle in action: panic reports and compiler messages go to stderr for exactly this reason.)

Best practice

Program output goes to println!. Debugging chatter, warnings, and progress notes go to eprintln! (or dbg!, below). Adopt the split now, while it's free; untangling the two streams later, in a program whose output other programs consume, is a chore with your name on it.

dbg!, the purpose-built one

Hand-rolled debug prints have a tedium tax: you type eprintln!("x is {x}"), then wonder which x is 7 you're looking at when three of them print. The dbg! macro pays the tax for you. Give it an expression and it prints the file, the line, the expression's own source text, and its value, to stderr:

fn main() {
    let price = 100;
    let tax = price / 10;
    dbg!(price + tax);
}
[src/main.rs:4:5] price + tax = 110

Location, code, value, no formatting strings, no labels to invent. And one more trick that makes it special: dbg! returns the value it printed, so it can wrap an expression in place without changing what the code computes:

fn main() {
    let price = 100;
    let total = dbg!(price / 10) + price;
    println!("total: {total}");
}
[src/main.rs:3:17] price / 10 = 10
total: 110

The division still happened, total still got 110, and we X-rayed the intermediate value in passing. No restructuring, no temporary variables; wrap the suspicious subexpression, read the report, unwrap it when done. This wrap-in-place trick is what makes dbg! the halving strategy's perfect partner: any seam from lesson 3.3 can be observed by wrapping it.

A worked hunt

The shipping bug. Expected total for a 100-coin order: 115 plus tax. The program says:

fn main() {
    let price = 100;
    let with_shipping = add_shipping(price);
    let total = add_tax(price);
    println!("total: {total}");
}

fn add_tax(amount: i32) -> i32 {
    amount + amount / 10
}

fn add_shipping(amount: i32) -> i32 {
    amount + 15
}
total: 110

110 is wrong (115 plus its tax should be 126), so: reproduce ✓, now locate. The pipeline is add_shipping then add_tax; check the seam by wrapping the value flowing into the tax step. Inside add_tax, wrap the parameter:

fn add_tax(amount: i32) -> i32 {
    dbg!(amount);
    amount + amount / 10
}
[src/main.rs:9:5] amount = 100
total: 110

There's the evidence: add_tax received 100, the raw price, not the 115 that add_shipping produced. The bug isn't in either function's math; it's at the wiring in main, where sharp eyes will spot add_tax(price) getting the wrong variable. It should be add_tax(with_shipping). One observation, root cause cornered; compare that to re-reading both functions' arithmetic five times (which is where staring would have taken you, since the arithmetic was never wrong).

The fix and retest: with add_tax(with_shipping), the program prints total: 126, the dbg! line comes out, done. Worth noticing: the compiler had been hinting all along, with an unused-variable warning for with_shipping, computed and never read. Warnings are clues (lesson 0.11 said they're symptoms); a bug hunt should always start by reading them.

Warning

dbg! is for temporary instrumentation, and its output format isn't something to build on (the docs explicitly decline to guarantee it). Before considering work finished, search the project for dbg! and clear the probes out; tidy code narrates only when asked. Your future teammates will judge a committed dbg! the way chefs judge a thumbprint in the plating. Fairly.

For advanced readers

For programs that want permanent, switchable narration (servers logging requests, tools with a --verbose flag), the ecosystem has dedicated logging crates with levels, timestamps, and filtering. Appendix A.1 points at the standard choices. The dividing line: dbg! is scaffolding you remove; logging is plumbing you design.

Quiz time

Question #1

Your program writes a report to stdout, and a teammate runs it as cargo run > report.txt. Which of your messages do they see on screen: the println! ones or the eprintln!/dbg! ones, and why does the difference matter here?

Show solution

Only the eprintln!/dbg! messages (stderr) appear on screen; all println! output went into report.txt. Which is exactly right: the file holds the clean product, the screen shows the chatter. If debug prints had used println!, they'd be inside the report.

Question #2

What does this print, in full?

fn main() {
    let n = 6;
    let doubled = dbg!(n * 2);
    println!("{doubled}");
}
Show solution
[src/main.rs:3:19] n * 2 = 12
12

The dbg! reports location, expression text, and value (to stderr), then hands the 12 through to doubled, which prints normally (to stdout).

Question #3

In the worked example, why was wrapping add_tax's parameter a smarter first probe than wrapping the arithmetic inside either function?

Show solution

It tested the seam between stages, per the halving strategy: one observation that decides whether the wrongness already existed before add_tax. It did (100 instead of 115), instantly clearing both functions' internals and pointing at the wiring. Probing the arithmetic first would have examined two innocent suspects in detail.

Prints make the program narrate. The next lesson gets you the same evidence without editing the program at all: pausing it live, mid-run, and looking around.