charmbracelet/bubbletea · warning · ErrProgramKilled

program was killed

Error message

program was killed

What it means

ErrProgramKilled is a sentinel returned by Program.Run when the program was terminated abnormally rather than quitting gracefully: someone called Program.Kill(), the context passed to RunContext/WithContext was cancelled, or the internal event loop failed. It signals that shutdown skipped the final render and terminal restoration ran in 'kill' mode. It is frequently wrapped together with the underlying cause (a context error or the error that killed the loop).

Source

Thrown at tea.go:42

	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"syscall"
	"time"

	"github.com/charmbracelet/colorprofile"
	uv "github.com/charmbracelet/ultraviolet"
	"github.com/charmbracelet/x/ansi"
	"github.com/charmbracelet/x/term"
	"github.com/muesli/cancelreader"
)

// ErrProgramPanic is returned by [Program.Run] when the program recovers from a panic.
var ErrProgramPanic = errors.New("program experienced a panic")

// ErrProgramKilled is returned by [Program.Run] when the program gets killed.
var ErrProgramKilled = errors.New("program was killed")

// ErrInterrupted is returned by [Program.Run] when the program get a SIGINT
// signal, or when it receives a [InterruptMsg].
var ErrInterrupted = errors.New("program was interrupted")

// Msg contain data from the result of a IO operation. Msgs trigger the update
// function and, henceforth, the UI.
type Msg = uv.Event

// Model contains the program's state as well as its core functions.
type Model interface {
	// Init is the first function that will be called. It returns an optional
	// initial command. To not perform an initial command return nil.
	Init() Cmd

	// Update is called when a message is received. Use it to inspect messages
	// and, in response, update the model and/or send a command.
	Update(Msg) (Model, Cmd)

View on GitHub (pinned to 351d2159f8)

Solutions

  1. If you intended a clean exit, use p.Quit() or tea.Quit cmd instead of p.Kill()/context cancel
  2. If cancellation is expected, treat errors.Is(err, tea.ErrProgramKilled) as a normal shutdown path, not a failure
  3. If it wraps a real cause, unwrap with errors.Unwrap / errors.Is to find the underlying error and fix that (often an input/renderer failure)
  4. Ensure you are not cancelling the parent context while Run is still expected to render its final frame

Example fix

// before
p := tea.NewProgram(m)
go func() { time.AfterFunc(time.Second, func() { p.Kill() }) }()

// after
p := tea.NewProgram(m)
go func() { time.AfterFunc(time.Second, func() { p.Quit() }) }()
Defensive patterns

Strategy: try-catch

Validate before calling

// Decide the shutdown channel before running:
// graceful -> p.Quit(); forced -> p.Kill()/ctx cancel (yields ErrProgramKilled)

Type guard

func isProgramKilled(err error) bool {
    return err != nil && errors.Is(err, tea.ErrProgramKilled)
}

Try / catch

_, err := p.Run()
if isProgramKilled(err) {
    // unwrap cause if present
    cause := errors.Unwrap(err)
    _ = cause
    // treat as abnormal-but-expected shutdown
    return nil
}

Prevention

When it happens

Trigger: Calling p.Kill() from another goroutine; cancelling the context given to tea.NewProgram(...).RunContext or tea.WithContext; the eventLoop returning a non-nil error (input read failure, renderer failure); a recovered panic (wrapped as ErrProgramKilled: ErrProgramPanic).

Common situations: Embedding a Bubble Tea program in a server where a supervisor cancels the ctx on shutdown; test harnesses that time out and cancel; calling Kill instead of Quit to stop a stuck program; SSH sessions (Wish) dropping and cancelling per-connection contexts.

Related errors


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