charmbracelet/bubbletea · error

bubbletea: error writing to screen: %w

Error message

bubbletea: error writing to screen: %w

What it means

Returned during renderer close when the buffered output (s.buf) could not be copied to the destination writer (s.w) with io.Copy after the screen flush. This is the last-chance write of remaining rendered content at teardown; failing here means the final frame or reset bytes never reached the terminal. The error wraps the underlying write error (often EPIPE, ErrClosed, or an IO timeout).

Source

Thrown at cursed_renderer.go:231

			_, _ = s.scr.WriteString(ansi.ResetProgressBar)
		}
	}

	if s.cellbuf.Method == ansi.GraphemeWidth {
		// Make sure to turn off Unicode mode (2027)
		_, _ = s.scr.WriteString(ansi.ResetModeUnicodeCore)
	}

	if err := s.scr.Flush(); err != nil {
		return fmt.Errorf("bubbletea: error closing screen writer: %w", err)
	}

	if s.buf.Len() > 0 {
		if s.logger != nil {
			s.logger.Printf("output: %q", s.buf.String())
		}
		if _, err := io.Copy(s.w, &s.buf); err != nil {
			return fmt.Errorf("bubbletea: error writing to screen: %w", err)
		}
		s.buf.Reset()
	}

	x, y := s.scr.Position()

	// We want to clear the renderer state but not the cursor position. This is
	// because we might be putting the tea process in the background, run some
	// other process, and then return to the tea process. We want to keep the
	// cursor position so that we can continue where we left off.
	reset(s)
	s.scr.SetPosition(x, y)

	return nil
}

// writeString implements renderer.
func (s *cursedRenderer) writeString(str string) (int, error) {

View on GitHub (pinned to 351d2159f8)

Solutions

  1. Keep the output writer open for the entire lifetime of the program, closing it only after Run returns
  2. For non-interactive runs use tea.WithoutRenderer() so no screen writes are attempted
  3. Debounce or retry if writing to a flaky custom writer; make your writer return clearer errors
  4. If the pipe consumer exiting early is expected, ignore ErrClosed/EPIPE on shutdown

Example fix

// before
w := getWriter() // may be closed by another goroutine
p := tea.NewProgram(m, tea.WithOutput(w))
_, err := p.Run()

// after
w := getWriter()
p := tea.NewProgram(m, tea.WithOutput(w))
_, _ = p.Run() // consume remaining writes first
w.Close()      // close only afterwards
Defensive patterns

Strategy: fallback

Validate before calling

// Keep ownership discipline: close output after Run
done := make(chan struct{})
go func() { defer close(done); p.Run() }()
// ... later: <-done; w.Close()

Type guard

func benignWriteFailure(err error) bool {
    return errors.Is(err, os.ErrClosed) || errors.Is(err, syscall.EPIPE) || errors.Is(err, io.EOF)
}

Try / catch

_, err := p.Run()
if err != nil && strings.Contains(err.Error(), "error writing to screen") {
    if benignWriteFailure(errors.Unwrap(errors.Unwrap(err))) { err = nil }
}

Prevention

When it happens

Trigger: io.Copy(s.w, &s.buf) failing during renderer close: destination writer closed, broken pipe, pty gone, or a custom tea.WithOutput writer returning an error. Happens after a render cycle buffered content that was never delivered.

Common situations: Output piped to a process that exited (`| head`); terminal window closed while the program is shutting down; custom output writer (network socket, bytes.Buffer wrapper) returning errors; CI environments where stdout detaches mid-run.

Related errors


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