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
- Use tea.WithInput(bytes.NewReader(...)) style readers only where supported, or wrap scripted input differently (feed via p.Send instead of a fake reader)
- Raise fd limits (ulimit -n) if EMFILE
- In seccomp-restricted containers, allow epoll syscalls or run with tea.WithInput(nil)
- 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
- For scripted input prefer p.Send with key messages over fake readers on fragile platforms
- Raise RLIMIT_NOFILE in fd-hungry daemons
- Allow epoll in container seccomp profiles
- Pin a recent bubbletea/cancelreader version for platform fixes
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
- program experienced a panic
- program was killed
- program was interrupted
- bubbletea: InitialModel cannot be nil
- bubbletea: error closing screen writer: %w
AI-assisted analysis of charmbracelet/bubbletea@351d2159f8 (2026-08-15).
Data as JSON: /api/errors/cb9af73e0b0c5df5.
Report an issue: GitHub.