Skip to content

bug: screen() commands silently swallow errors from async useEffect callbacks #174

Description

@zrosenbauer

Summary

Errors thrown inside async useEffect callbacks in screen() commands are silently swallowed by Ink/React. The process exits with no error output — no stack trace, no message, nothing. This makes debugging screen-based commands extremely difficult.

Reproduction

// In any screen() component's useEffect:
useEffect(() => {
  async function init() {
    throw new Error('This error is never seen')
  }
  init() // fire-and-forget async — rejection swallowed by React
}, [])

The screen renders briefly (e.g., a loading spinner), then exits silently back to the shell.

Root Cause

  1. init() is an async function called without .catch() inside useEffect
  2. The rejected promise becomes an unhandled rejection inside React's effect lifecycle
  3. Ink/React catches it internally and never re-throws to the process level
  4. process.on('unhandledRejection') never fires
  5. The fullscreen TUI exits, clearing the terminal — no trace of the error remains

This is a known Ink behavior: vadimdemedes/ink#288, vadimdemedes/ink#542

Expected Behavior

When a screen() command's component tree throws an unhandled error, kidd-cli should:

  1. Catch the error at the framework level (React error boundary or waitUntilExit rejection)
  2. Exit fullscreen mode cleanly (let Ink's cleanup run)
  3. Then print the error to stderr so the user can see it
  4. Exit with code 1

Proposed Fix

kidd-cli's screen() implementation should wrap the rendered component tree in a React error boundary that:

Alternatively, screen() could install its own process.on('unhandledRejection') handler scoped to the render lifecycle.

Workaround

CLI authors can work around this by:

  1. Adding .catch() to all async calls in useEffect
  2. Manually calling reportCrash() + process.exit(1) in the catch handler
  3. Using the hook's error state to display the error in the TUI

This is what we're doing in zpress — see thebytefarm/ciderpress#105.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions