charmbracelet/bubbletea · info · ErrInterrupted

program was interrupted

Error message

program was interrupted

What it means

ErrInterrupted is returned by Program.Run when the program received SIGINT (Ctrl+C) or an explicit tea.InterruptMsg, and exited because of it. It distinguishes a user-initiated interrupt from a graceful quit (which returns nil) so callers can decide exit codes. It is only produced when the built-in signal handler is active (not disabled with WithoutSignalHandler).

Source

Thrown at tea.go:46

	"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 renders the program's UI, which can be a string or a [Layer]. The
	// view is rendered after every Update.
	View() View

View on GitHub (pinned to 351d2159f8)

Solutions

  1. If Ctrl+C should be handled by the model instead of exiting, filter it in Update on KeyMsg with tea.KeyCtrlC and return tea.Quit yourself (Run then returns nil)
  2. If you want the interrupt status, check errors.Is(err, tea.ErrInterrupted) after Run and map it to exit code 130
  3. Use tea.WithoutSignalHandler() and wire your own signal handling if you need custom SIGINT semantics
  4. Do not treat this error as a bug — it is a normal termination signal

Example fix

// before
if _, err := p.Run(); err != nil {
    log.Fatal(err) // treats Ctrl+C as fatal
}

// after
_, err := p.Run()
if err != nil && !errors.Is(err, tea.ErrInterrupted) {
    log.Fatal(err)
}
os.Exit(130)
Defensive patterns

Strategy: try-catch

Validate before calling

// If you need custom Ctrl+C behavior, install no changes here — handle KeyMsg
// in Update and don't let the default quit path surprise you:
// p := tea.NewProgram(m) // default signal handler active

Type guard

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

Try / catch

_, err := p.Run()
if isInterrupted(err) {
    os.Exit(130) // conventional SIGINT exit status
}
if err != nil { log.Fatal(err) }

Prevention

When it happens

Trigger: User presses Ctrl+C while the program owns the terminal in raw mode; the process receives SIGINT from the shell or a supervisor; the model sends tea.InterruptMsg (e.g. via a tea.Interrupt command) to terminate with this status.

Common situations: CLI tools wanting exit code 130 on Ctrl+C; programs where Ctrl+C should mean 'cancel current operation' but the default handler quits; running under debuggers or supervisors that deliver SIGINT; shells sending SIGINT to whole process groups.

Related errors


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