Skip to content

What dbg! does

Level: 101 → 201 · working knowledge

One line: dbg! is not a shorter println!("{:?}") — it hands your value back so you can wrap any expression in place, it captures the source text of what you asked about, and it writes to stderr, which is why half of what surprises people about it never appears in the output they are reading.


It gives the value back

This is the property everything else follows from:

let doubled = dbg!(2 + 3) * 10;   // doubled == 50

dbg! evaluates its argument, prints it, and returns it. So you can drop it into the middle of an expression you already have — no temporary variable, no restructuring, and you delete it by deleting six characters. println! returns (), so the same edit would not compile.

It captures the expression, not just the value

[what_dbg_does.rs:17:19] 2 + 3 = 5

Three things: file:line:col, the source text of the expression, and the value. 2 + 3 is not a string you passed — the macro captured it. That is the real argument against hand-rolling println!("x = {:?}", x): the label there is a string literal, so it silently goes stale the moment you rename x or paste the line somewhere else. A dbg! label cannot lie about what it printed.

The three come from std's own macros — dbg! is an eprintln! of file!(), line!(), column!(), stringify! of the expression, and the value under {:#?}. So the file is whatever path the compiler was handed: the bare name when you run rustc what_dbg_does.rs in the example's folder, src/main.rs under Cargo, which passes paths relative to the workspace root (a workspace member named main prints main/src/main.rs). Line and column count from 1 to the d of dbg!: line 17 of the example, and column 19, after four spaces of indent and let doubled =.

dbg!() with no argument prints just the location, which is a decent "did we get here" probe.

It writes to stderr

Not stdout. Three consequences, in increasing order of how long they take to work out:

  • cargo run > out.txt keeps your program's real output clean and leaves the debugging on the terminal. That is the design.
  • 2>/dev/null makes it vanish, and a pipe reorders it — stdout is block-buffered when piped, stderr never is, so dbg! lines can appear ahead of println! lines that ran first.
  • In this repo it means dbg! output is unrecordable. tools/run_examples.py captures stdout only, so a lesson that demonstrates dbg! has to describe it with println! to have an answer key at all. The example on this page does exactly that, and says so where it happens.

It always formats with {:#?}

dbg! is hard-wired to the alternate (pretty) form. For a derived Debug that is a gift. For a hand-written one it is a trap:

Flat with {:?}   -> Boxy fake1 fake2
Flat with {:#?}  -> Boxy fake1 fake2

Identical — because that impl is a plain write! chain that never asks f.alternate(). The flag arrived and was ignored, so dbg! silently gets the flat version, and a trailing writeln! in the impl adds a blank line on top of the newline dbg! already prints. f.debug_struct(…) handles the flag for you, which is the first reason to reach for it; f.alternate() is the question to ask if you write the branch yourself.

It moves a non-Copy argument

dbg!(ballot);           // moved in — and you did not catch the return
println!("{}", ballot); // error[E0382]: borrow of moved value

It hands the value back, so let b = dbg!(b); is fine. But a bare dbg!(b); on its own line drops the value at the end of the statement. dbg!(&b) is the habit — nothing moves, and the printed label reads &b, which is honest about what happened. This is why dbg! in real code is nearly always written with an &.

It survives --release

Unlike debug_assert!, dbg! has no cfg gate. Compile with -O and it still prints. It is a thing you delete, not a logging macro you leave in — nothing but code review will catch one you forgot.

The confusion worth naming: field vs whole value

It is easy to conclude from experiment that dbg! is lenient about some structs and strict about others. It is not. These four fail identically, with the same E0277:

dbg!(named_struct)        println!("{:?}", named_struct)
dbg!(unit_struct)         println!("{:?}", unit_struct)

dbg!(b.score) works with no derive because u8 implements Debug — you named a field, so the field's type is what needs it. A unit struct only looks stricter: it has no field to name, so the lenient move is unavailable. There is no unit-struct rule and no dbg!-specific rule. Whatever you name must implement Debug.

If you are coming from another language

Python. The closest thing is print(f"{x=}"), which also captures the expression text — and icecream's ic() is a near-exact match, returning its argument the same way. What Python has no equivalent of is the move: ic(obj) never costs you the object.

ABAP. No equivalent. WRITE goes to the list, BREAK-POINT stops the program; there is nothing that prints a value inline and gives it back so the surrounding expression still works.


Practice

Find both traps, then fix them. Take a struct with a hand-written Debug that is a plain write! chain ending in writeln!.

  1. Print it with {:?} and with {:#?}. Why are they identical, and what did dbg! therefore get?
  2. Rebuild the impl with f.debug_struct(…). What changed, and what did you no longer have to write?
  3. Write the same thing by hand a third time, honouring the flag yourself. Which function tells you it is set?
  4. Now dbg!(data); on its own line, and use data afterwards. Read the error. Fix it two ways — one that rebinds and one that borrows — and say which you would put in real code, and why.
Solution

what_dbg_does_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.

//! Kata solution: the two `dbg!` traps — the alternate flag, and the move.
//!
//!   rustc --edition 2024 what_dbg_does_kata.rs -o /tmp/wddk && /tmp/wddk

use std::fmt;

#[derive(Clone, Debug)] // Debug here is for TRAP 2; the wrappers below write their own
struct Data {
    name: String,
    bones: Vec<String>,
}

// (a) The hand-written Debug that ignores the alternate flag. This is the
//     common shape, and it is wrong in a way nothing warns about.
struct Flat(Data);
impl fmt::Debug for Flat {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", &self.0.name)?;
        for bone in &self.0.bones {
            write!(f, " {bone}")?;
        }
        writeln!(f) // and a trailing newline nobody asked for
    }
}

// (b) The same thing built with the Formatter's own helper, which asks.
struct Honest(Data);
impl fmt::Debug for Honest {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Data")
            .field("name", &self.0.name)
            .field("bones", &self.0.bones)
            .finish()
    }
}

// (c) Hand-written, but honouring alternate() explicitly — what debug_struct
//     is doing for you underneath.
struct Manual(Data);
impl fmt::Debug for Manual {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            write!(f, "{}\n  bones: {:?}", self.0.name, self.0.bones)
        } else {
            write!(f, "{} {:?}", self.0.name, self.0.bones)
        }
    }
}

fn main() {
    let d = Data { name: "Boxy".into(), bones: vec!["fake1".into(), "fake2".into()] };

    println!("TRAP 1 — a hand-written Debug that never asks f.alternate()\n");
    println!("  Flat with {{:?}}   -> {:?}", Flat(d.clone()));
    println!("  Flat with {{:#?}}  -> {:#?}", Flat(d.clone()));
    println!("  Identical. The alternate flag arrived and the impl ignored it.");
    println!("  dbg! is hard-wired to {{:#?}}, so dbg! gets the flat one too — and the");
    println!("  stray writeln! adds a blank line on top of the newline dbg! prints.\n");

    println!("  Honest with {{:?}}  -> {:?}", Honest(d.clone()));
    println!("  Honest with {{:#?}} ->");
    println!("{:#?}", Honest(d.clone()));
    println!("  f.debug_struct() checks the flag for you. Reach for it first.\n");

    println!("  Manual with {{:?}}  -> {:?}", Manual(d.clone()));
    println!("  Manual with {{:#?}} -> {:#?}", Manual(d.clone()));
    println!("  ...and if you must write it by hand, f.alternate() is the question.\n");

    println!("TRAP 2 — dbg! moves a non-Copy argument\n");
    println!("  dbg!(d);        // moves d in. It hands the value back...");
    println!("  println!(\"{{}}\", d.name);   // ...but you did not catch it: E0382");
    println!();
    println!("  Two fixes, and they mean different things:");
    let d = dbg!(d); //        catch the value back
    println!("    let d = dbg!(d);   rebind — d is alive because you took the return");
    dbg!(&d); //                borrow instead
    println!("    dbg!(&d);          borrow — nothing moved at all, and the printed");
    println!("                       label shows `&d` rather than `d`");
    println!("  Reach for the borrow. It reads as an inspection, which is what it is.");
    println!("  still own it: {} with {} bones", d.name, d.bones.len());

    println!("\n(Both dbg! calls above wrote to stderr, so they are absent from this");
    println!("page's recorded output — see section 3 of the lesson.)");
}

Verified output of what_dbg_does_kata.rs — regenerated by tools/run_examples.py, never hand-typed.

TRAP 1 — a hand-written Debug that never asks f.alternate()

  Flat with {:?}   -> Boxy fake1 fake2

  Flat with {:#?}  -> Boxy fake1 fake2

  Identical. The alternate flag arrived and the impl ignored it.
  dbg! is hard-wired to {:#?}, so dbg! gets the flat one too — and the
  stray writeln! adds a blank line on top of the newline dbg! prints.

  Honest with {:?}  -> Data { name: "Boxy", bones: ["fake1", "fake2"] }
  Honest with {:#?} ->
Data {
    name: "Boxy",
    bones: [
        "fake1",
        "fake2",
    ],
}
  f.debug_struct() checks the flag for you. Reach for it first.

  Manual with {:?}  -> Boxy ["fake1", "fake2"]
  Manual with {:#?} -> Boxy
  bones: ["fake1", "fake2"]
  ...and if you must write it by hand, f.alternate() is the question.

TRAP 2 — dbg! moves a non-Copy argument

  dbg!(d);        // moves d in. It hands the value back...
  println!("{}", d.name);   // ...but you did not catch it: E0382

  Two fixes, and they mean different things:
    let d = dbg!(d);   rebind — d is alive because you took the return
    dbg!(&d);          borrow — nothing moved at all, and the printed
                       label shows `&d` rather than `d`
  Reach for the borrow. It reads as an inspection, which is what it is.
  still own it: Boxy with 2 bones

(Both dbg! calls above wrote to stderr, so they are absent from this
page's recorded output — see section 3 of the lesson.)

The verified output

Every dbg! line this program prints goes to stderr, and the recorded key below is stdout only — which is section 3 demonstrating itself. Run it to see the other half.

Verified output of what_dbg_does.rs — regenerated by tools/run_examples.py, never hand-typed.

1. `dbg!` returns its argument, and that is the whole point
   let doubled = dbg!(2 + 3) * 10;   ->  50
   It evaluated to 5 and handed it straight back, so you can wrap any
   sub-expression without restructuring the code around it.
   println! returns (), so the same move would not compile.

2. It prints three things, not one
   file:line:col, the EXPRESSION SOURCE TEXT, and the value:
       [what_dbg_does.rs:17:19] 2 + 3 = 5
   Line 17 is the call in section 1, and column 19 is where `dbg!`
   starts on it, counting from 1: four spaces and `let doubled = ` come first.
   `2 + 3` is not a string you passed — the macro captured the source.
   That is why `dbg!(x)` beats `println!("x = {:?}", x)`: the label
   cannot go stale when you rename x.

3. It writes to STDERR
   A dbg! line just fired above and you may not see it here — `2>/dev/null`
   hides it, and a pipe reorders it, because stdout is block-buffered when
   piped while stderr never is.
   Practical consequence: `cargo run > out.txt` keeps your program's real
   output clean and leaves the debugging on the terminal. In THIS repo it
   means run_examples.py cannot record dbg! output at all — it captures
   stdout only, so a lesson must print with println! to have an answer key.

4. It formats with `{:#?}`, always
   dbg! is hard-wired to the alternate (pretty) form, one field per line.
   For a derived Debug that is a gift. For a HAND-WRITTEN one it is a trap:
   if your impl never asks f.alternate(), `{:?}` and `{:#?}` print the
   same thing, and dbg! silently gets the flat version.

5. It MOVES a non-Copy argument
   dbg!(owned) took ownership. It gives the value back, so `let x = dbg!(x)`
   is fine — but a bare `dbg!(owned);` on its own line drops it, and the
   next use is E0382. `dbg!(&owned)` is the habit: borrow, print, move on.
   still here: Ben scored 3

6. It is NOT removed in release builds
   Unlike debug_assert!, dbg! has no cfg gate. Compile with -O and it still
   prints. It is a thing you delete, not a logging macro you leave in.
   (`cargo build --release` will not save you; a code review has to.)

7. The confusion worth naming: field vs whole value
   dbg!(b.score) works with no derive, because u8 implements Debug.
   dbg!(b) needs Ballot to implement it. These four are the SAME error:
       dbg!(named_struct)        println!("{:?}", named_struct)
       dbg!(unit_struct)         println!("{:?}", unit_struct)
   A unit struct only LOOKS stricter — it has no field to name, so the
   lenient move is not available. There is no unit-struct rule, and no
   dbg!-specific rule: whatever you NAME must implement Debug.
   (that dbg! fired on stderr too — a u8, no derive needed)

See also

Po polsku

Najczęstsze polskie skrócenie brzmi „dbg! to krótszy println!("{:?}")” i jest fałszywe w miejscu, które decyduje o wszystkim: dbg! oddaje swój argument. Dzięki temu można je owinąć wokół dowolnego podwyrażenia bez przebudowywania kodu — let doubled = dbg!(2 + 3) * 10; nadal daje 50, a usuwa się je, kasując sześć znaków. println! zwraca (), więc ta sama sztuczka po prostu się nie skompiluje. Drugą różnicą jest to, że makro przechwytuje tekst źródłowy wyrażenia, nie samą wartość: wypisuje [what_dbg_does.rs:17:19] 2 + 3 = 5, czyli plik, wiersz, kolumnę, treść wyrażenia i wynik. Etykieta w ręcznie pisanym println!("x = {:?}", x) to zwykły literał, który po zmianie nazwy zmiennej zaczyna kłamać — etykieta dbg! nie może.

Rzecz, która najczęściej zabiera pół godziny: dbg! pisze na standardowe wyjście błędów (stderr), a nie na standardowe wyjście. To jest zamierzone — cargo run > out.txt zostawia prawdziwy wynik programu w pliku, a diagnostykę na terminalu — ale ma dwa nieoczywiste skutki. 2>/dev/null sprawia, że linijki dbg! znikają bez śladu, a przy przekierowaniu do potoku kolejność się rozjeżdża: stdout jest wtedy buforowane blokowo, stderr nigdy, więc wiersz z dbg! potrafi pojawić się przed println!, który wykonał się wcześniej. W tym repozytorium wychodzi to jeszcze dobitniej: tools/run_examples.py zapisuje wyłącznie stdout, więc wyjścia dbg! nie da się utrwalić w kluczu odpowiedzi i lekcja musi je opisać przez println!.

Na koniec dwie pułapki i jedna reguła, której nie ma. Pierwsza pułapka: przy argumencie, który nie jest Copy, dbg! przejmuje własność — samo dbg!(ballot); w osobnym wierszu wprawdzie oddaje wartość, ale nikt jej nie łapie, więc ginie na końcu instrukcji i kolejne użycie to error[E0382]: borrow of moved value. Nawykiem jest dbg!(&ballot): nic się nie przenosi, a wypisana etykieta uczciwie pokazuje &ballot. Druga: dbg! jest na sztywno związane z {:#?}, więc ręcznie napisany impl Debug, który nigdy nie pyta f.alternate(), wypisze przez dbg! dokładnie tę samą płaską postać co przy {:?}f.debug_struct(…) sprawdza tę flagę za ciebie. A regułą, której nie ma, jest jakakolwiek osobna surowość dbg! wobec struktur: dbg!(b.score) działa bez #[derive(Debug)], bo to u8 musi implementować Debug, a pusta struktura wygląda na surowszą tylko dlatego, że nie ma w niej pola, które dałoby się nazwać. I pamiętaj, że dbg! nie znika w --release — w odróżnieniu od debug_assert! nie ma bramki cfg, więc to makro do kasowania, nie do zostawiania w kodzie.

Szukaj po polsku: makro dbg! w Ruscie · standardowe wyjście błędów · debugowanie printami · rust dbg macro stderr · rust E0382 borrow of moved value