charmbracelet/bubbletea · error

bubbletea: could not create cancelable reader

Error message

bubbletea: could not create cancelable reader: %w

What it means

Returned by initInputReader when uv.NewCancelReader(p.input) fails — Bubble Tea could not construct a cancellable reader over the configured input, which it needs so the read loop can be stopped on shutdown. On Unix this allocates an epoll-based cancelreader; on unsupported or constrained environments construction fails and Program.Run aborts right after terminal setup.

Solutions

  1. Use tea.WithInput(bytes.NewReader(...)) style readers only where supported, or wrap scripted input differently (feed via p.Send instead of a fake reader)
  2. Raise fd limits (ulimit -n) if EMFILE
  3. In seccomp-restricted containers, allow epoll syscalls or run with tea.WithInput(nil)
  4. Report platform-specific construction failures upstream with the wrapped error

Example fix

// before
p := tea.NewProgram(m, tea.WithInput(bytes.NewBufferString("q")))
_, err := p.Run() // cancelreader construction may fail on this platform

// after
p := tea.NewProgram(m, tea.WithInput(nil))
go func() {
    time.Sleep(500 * time.Millisecond)
    p.Send(key.NewMsg(key.WithKeys("q"))) // script input as messages
}()
_, err := p.Run()
Defensive patterns

Strategy: fallback

Validate before calling

// Probe cancelreader viability cheaply: if construction fails, fall back
// to message-driven input:
func inputReaderOK(r io.Reader) bool {
    cr, err := cancelreader.NewReader(r)
    if err != nil { return false }
    _ = cr.Close()
    return true
}

Type guard

func isCancelReaderFailure(err error) bool {
    return err != nil && strings.Contains(err.Error(), "could not create cancelable reader")
}

Try / catch

_, err := p.Run()
if isCancelReaderFailure(err) {
    // fallback: no reader, drive via Send()
    p2 := tea.NewProgram(m, tea.WithInput(nil))
    go func() { p2.Send(key.NewMsg(key.WithKeys("q"))) }()
    _, err = p2.Run()
}
if err != nil { log.Fatal(err) }

Prevention

When it happens

Trigger: Input fd not pollable (regular files, some /dev/null setups, closed fds on platforms where cancelreader needs a valid pollable fd); ulimit exhaustion (EMFILE) preventing epoll/fd creation; exotic Unixes lacking the required syscalls; certain container/seccomp profiles blocking epoll_create.

Common situations: tea.WithInput fed a regular file or bytes.Buffer on platforms where the cancelreader needs extra fds; very high fd counts hitting limits; hardened containers blocking epoll; older kernels/sandboxed environments (gVisor quirks).

Related errors


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

Appendix: source

Thrown at tty.go:71

}

// initInputReader (re)commences reading inputs.
func (p *Program) initInputReader(cancel bool) error {
	if cancel && p.cancelReader != nil {
		p.cancelReader.Cancel()
		p.waitForReadLoop()
	}

	term := p.environ.Getenv("TERM")

	// Initialize the input reader.
	// This need to be done after the terminal has been initialized and set to
	// raw mode.

	var err error
	p.cancelReader, err = uv.NewCancelReader(p.input)
	if err != nil {
		return fmt.Errorf("bubbletea: could not create cancelable reader: %w", err)
	}

	drv := uv.NewTerminalReader(p.cancelReader, term)
	drv.SetLogger(p.logger)
	p.inputScanner = drv
	p.readLoopDone = make(chan struct{})

	go p.readLoop()

	return nil
}

func (p *Program) readLoop() {
	defer close(p.readLoopDone)

	if err := p.inputScanner.StreamEvents(p.ctx, p.msgs); err != nil {
		select {
		case <-p.ctx.Done():

View on GitHub (pinned to 351d2159f8)