junegunn/fzf · error

failed to read %s

Error message

failed to read %s

What it means

The light TUI renderer could not read a character from the terminal: r.getch returned a non-OK result that is not cancellation, so the renderer closes itself and reports 'failed to read /dev/tty' (DefaultTtyDevice). It usually means stdin's TTY went away or a non-blocking read hit a hard error.

Source

Thrown at src/tui/light.go:352

	env := os.Getenv(name)
	if len(env) == 0 {
		return defaultValue
	}
	return atoi(env, defaultValue)
}

func (r *LightRenderer) getBytes(cancellable bool) ([]byte, getCharResult, error) {
	return r.getBytesInternal(cancellable, r.buffer, false)
}

func (r *LightRenderer) getBytesInternal(cancellable bool, buffer []byte, nonblock bool) ([]byte, getCharResult, error) {
	c, result := r.getch(cancellable, nonblock)
	if result == getCharCancelled {
		return buffer, getCharCancelled, nil
	}
	if !nonblock && !result.ok() {
		r.Close()
		return nil, getCharError, errors.New("failed to read " + DefaultTtyDevice)
	}

	retries := 0
	if c == Esc.Int() || nonblock {
		retries = r.escDelay / escPollInterval
	}
	// A non-blocking read that found nothing has no byte to record. Recording
	// one would put a NUL in the middle of a reply still being assembled.
	if result.ok() {
		buffer = append(buffer, byte(c))
	}

	pc := c
	for {
		c, result = r.getch(false, true)
		if !result.ok() {
			if retries > 0 {
				retries--

View on GitHub (pinned to bd4efa277b)

Solutions

  1. Reproduce with a real terminal; if this happens in CI/tests, provide a pty (e.g. 'script -qc "fzf" /dev/null')
  2. If the terminal genuinely died, this error is expected — just let fzf exit non-zero
  3. For scripting without interaction, drive fzf via its --filter mode instead of the interactive renderer

Example fix

# before
fzf < /dev/null   # no tty for interaction
# after
echo -e 'a\nb' | fzf --filter 'a'
Defensive patterns

Strategy: fallback

Validate before calling

# offer a non-interactive path when no usable tty exists
if [ ! -t 0 ] || [ ! -t 1 ]; then
  printf '%s\n' "${ITEMS[@]}" | fzf --filter "$QUERY"
else
  printf '%s\n' "${ITEMS[@]}" | fzf --query "$QUERY"
fi

Prevention

When it happens

Trigger: fzf's light renderer reads input from the TTY and the read fails: the terminal was closed while fzf was running, the pty was torn down (session leader exited), or a raw-mode read on /dev/tty returned an error (EBADF/EIO).

Common situations: Running fzf under ssh and the connection drops; terminal multiplexers killing the pane; CI harnesses or test runners that provide no real TTY then close stdin; nested programs stealing then closing the tty.

Related errors


AI-assisted analysis of junegunn/fzf@bd4efa277b (2026-08-15). Data as JSON: /api/errors/d9ec92847cd259ec. Report an issue: GitHub.