charmbracelet/bubbletea · warning

error writing to output: %w

Error message

error writing to output: %w

What it means

Returned by Program.flush (tea.go:1234) when writing the accumulated command buffer (p.outputBuf) to p.output fails. This buffer carries terminal control sequences submitted via p.execute / p.batch — bracketed paste setup, mode queries (2026/2027), keyboard enhancements — so failing here means control sequences could not be delivered. It surfaces during shutdown's restoreTerminalState which deliberately ignores it, or during runtime where it can feed the kill path.

Source

Thrown at tea.go:1234

	_, _ = p.outputBuf.WriteString(seq)
	p.mu.Unlock()
}

// flush flushes the output buffer to the program output.
func (p *Program) flush() error {
	p.mu.Lock()
	defer p.mu.Unlock()

	if p.outputBuf.Len() == 0 {
		return nil
	}
	if p.logger != nil {
		p.logger.Printf("output: %q", p.outputBuf.String())
	}
	_, err := p.output.Write(p.outputBuf.Bytes())
	p.outputBuf.Reset()
	if err != nil {
		return fmt.Errorf("error writing to output: %w", err)
	}
	return nil
}

// shutdown performs operations to free up resources and restore the terminal
// to its original state.
func (p *Program) shutdown(kill bool) {
	p.shutdownOnce.Do(func() {
		p.cancel()

		// Wait for all handlers to finish.
		p.handlers.shutdown()

		// Check if the cancel reader has been setup before waiting and closing.
		if p.cancelReader != nil {
			// Wait for input loop to finish.
			if p.cancelReader.Cancel() {
				if !kill {

View on GitHub (pinned to 351d2159f8)

Solutions

  1. Keep p.output valid and open through the whole Run lifecycle
  2. Skip terminal capability queries when output is not a TTY (the library gates most, but custom p.execute calls are yours)
  3. Handle EPIPE as a shutdown signal, not an error to log loudly
  4. For scripted/headless runs use WithoutRenderer and don't queue control sequences

Example fix

// before
p.execute(ansi.RequestModeSynchronizedOutput) // queued, flush may fail

// after
if term.IsTerminal(os.Stdout.Fd()) {
    p.execute(ansi.RequestModeSynchronizedOutput)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Only queue control sequences on real terminals:
func isTTY(w io.Writer) bool {
    if f, ok := w.(*os.File); ok {
        return term.IsTerminal(f.Fd())
    }
    return false
}

Type guard

func isOutputFlushFailure(err error) bool {
    return err != nil && strings.Contains(err.Error(), "error writing to output")
}

Try / catch

_, err := p.Run()
if isOutputFlushFailure(err) {
    if isDeadPipe(errors.Unwrap(err)) { err = nil }
}
if err != nil { log.Print(err) }

Prevention

When it happens

Trigger: p.execute(...) queue flush hitting a closed/broken output writer; mode-query writes at startup (RequestModeSynchronizedOutput) when output is dead; custom tea.WithOutput writer erroring; EPIPE because the pipe consumer exited.

Common situations: Non-TTY output combined with mode queries; pipes closing early (`| head`); SSH/pty teardown racing startup; writers shared between processes that get closed by the other side.

Related errors


AI-assisted analysis of charmbracelet/bubbletea@351d2159f8 (2026-08-15). Data as JSON: /api/errors/c1b595fdddf61fbf. Report an issue: GitHub.