dbt-labs/dbt-core · error

failed to write header to stdout

Error message

failed to write header to stdout

What it means

handle_list_item_output writes the 'SELECTED NODES' delimiter header (once, guarded by an AtomicBool) using .expect('failed to write header to stdout'), panicking on write failure. The header write happens inside write_suspended while rendering `dbt list` output. A broken stdout makes listing output delivery fatal.

Source

Thrown at crates/dbt-common/src/tracing/layers/tui_layer.rs:1411

    }

    fn handle_list_item_output(&self, list_item: &ListItemOutput) {
        if self.show_options.contains(&ShowOptions::Nodes) || self.command == FsCommand::List {
            self.write_suspended(|| {
                let mut stdout = io::stdout().lock();

                // Only emit the decorative header when show_options is non-empty (i.e. not quiet).
                // In quiet mode show_options is cleared, so the header is suppressed while list
                // item content (the result payload) continues to be printed — matching dbt-core's
                // PrintEvent behaviour where only decorative chrome is stripped.
                if !self.show_options.is_empty()
                    && !self.list_header_emitted.swap(true, Ordering::Relaxed)
                {
                    let header =
                        format_delimiter(SELECTED_NODES_TITLE, self.max_term_line_width, true);
                    stdout
                        .write_all(format!("{}\n", header).as_bytes())
                        .expect("failed to write header to stdout");
                }

                // Print list item content (always — result payload survives quiet)
                stdout
                    .write_all(format!("{}\n", list_item.content).as_bytes())
                    .expect("failed to write to stdout");
            });
        }
    }

    fn handle_show_data_output(&self, show_data: &ShowDataOutput) {
        self.write_suspended(|| {
            let mut stdout = io::stdout().lock();

            stdout
                .write_all(format!("{}\n", show_data.content).as_bytes())
                .expect("failed to write show data to stdout");
        });

View on GitHub (pinned to 0267ce9170)

Solutions

  1. Consume the full pipe output or redirect to a file: `dbt list > nodes.txt`
  2. Use `dbt list | sed -n '1,5p'` alternatives carefully — prefer file redirection for truncation
  3. Verify the capturing process stays alive in CI
  4. Harden the layer to treat stdout errors as end-of-output rather than panic

Example fix

// before
.expect("failed to write header to stdout");
// after
if stdout.write_all(format!("{}\n", header).as_bytes()).is_err() {
    return; // stdout gone; stop emitting
}
Defensive patterns

Strategy: try-catch

Validate before calling

# capture the full listing to a file before truncating
dbt list > nodes.txt 2>&1 && head -n 5 nodes.txt

Type guard

fn stdout_writable() -> bool {
    use std::io::Write;
    io::stdout().flush().is_ok()
}

Try / catch

// catch pipe failure and fall back to file-based listing
dbt list 2>err.log || { grep -q 'failed to write header to stdout' err.log && dbt list > out.txt; }

Prevention

When it happens

Trigger: First list-item log record after quiet filtering (on_log_record) triggers the header write while the pipe consumer has exited or stdout is invalid.

Common situations: `dbt list | head -n 5` — head exits after 5 lines and the next write fails; listing into a closed socket; redirected output on a full disk.

Related errors


AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07). Data as JSON: /api/errors/f532a3053757e712. Report an issue: GitHub.