charmbracelet/bubbletea · error

bubbletea: could not create cancelable reader: %w

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.

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)

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.