charmbracelet/bubbletea · error

bubbletea: error closing screen writer: %w

Error message

bubbletea: error closing screen writer: %w

What it means

Returned by the cursed renderer's close path when the underlying screen writer's Flush fails while tearing down the renderer (Program shutting down or being closed). The close sequence writes terminal reset sequences (mouse modes, title, cursor style, colors, unicode mode 2027) and this error means those bytes could not be flushed to the output writer. It indicates the output channel (terminal, pty, or redirected writer) rejected writes at shutdown.

Source

Thrown at cursed_renderer.go:223

		if lv.BackgroundColor != nil {
			_, _ = s.scr.WriteString(ansi.ResetBackgroundColor)
		}
		if lv.ForegroundColor != nil {
			_, _ = s.scr.WriteString(ansi.ResetForegroundColor)
		}
		if lv.ProgressBar != nil && lv.ProgressBar.State != ProgressBarNone {
			_, _ = 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.

View on GitHub (pinned to 351d2159f8)

Solutions

  1. Ensure the output writer stays open until Program.Run returns
  2. If piping/redirecting intentionally, use tea.WithoutRenderer() or tea.WithInput(nil) for non-TTY runs instead of a full TUI
  3. Check for EPIPE and treat it as a benign shutdown error if the consumer is allowed to exit early
  4. If over SSH, handle disconnects by cancelling the program context so shutdown happens while the pty is still alive

Example fix

// before
p := tea.NewProgram(m, tea.WithOutput(w))
// w gets closed elsewhere while p.Run() is still shutting down

// after
done := make(chan struct{})
go func() { p.Run(); close(done) }()
// close w only after Run finished
closeWriterAfter(done)
Defensive patterns

Strategy: fallback

Validate before calling

// Validate the output channel before shutdown can hit it:
func isWritable(w io.Writer) bool {
    if f, ok := w.(*os.File); ok {
        return f.Fd() >= 0 // still has an fd
    }
    return true
}

Type guard

func isClosedWrite(err error) bool {
    return errors.Is(err, os.ErrClosed) || errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET)
}

Try / catch

if err := runErr; err != nil {
    if strings.Contains(err.Error(), "error closing screen writer") && isClosedWrite(errors.Unwrap(err)) {
        return // consumer exited: benign
    }
    log.Print(err)
}

Prevention

When it happens

Trigger: Program shutdown when stdout is a closed pipe or pty; running with tea.WithOutput to a network connection that dropped; a terminal that was closed before the program exited; EPIPE/ErrClosed from the output file during the final Flush in renderer close.

Common situations: Piping output to `head` or another short-lived process that exits first; SSH session disconnected before graceful shutdown; test harness closing the fake output writer early; running a Bubble Tea program with output redirected to a file on a full disk.

Related errors


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