Skip to content

feat(appkit): add global uncaughtException/unhandledRejection handlers#448

Closed
MarioCadenas wants to merge 3 commits into
mainfrom
feat/global-crash-handlers
Closed

feat(appkit): add global uncaughtException/unhandledRejection handlers#448
MarioCadenas wants to merge 3 commits into
mainfrom
feat/global-crash-handlers

Conversation

@MarioCadenas

Copy link
Copy Markdown
Collaborator

Problem

The server bootstrap registers SIGTERM/SIGINT handlers but had no global
uncaughtException / unhandledRejection handlers. An unhandled async error
would crash the Node process silently in production — no structured log, no
graceful drain of in-flight work.

What changed

In packages/appkit/src/plugins/server/index.ts, the signal handlers are now
registered via a new _registerProcessHandlers() method (called from start()
in the same spot SIGTERM/SIGINT were wired), which also adds:

  • uncaughtException — logs the error (with origin) via the existing
    server logger, then runs the same _gracefulShutdown() path the signal
    handlers use (drain plugins, close server) and exits non-zero. _gracefulShutdown
    now takes an exitCode (default 0) so the success path stays 0 while a
    fatal exception exits 1.
  • unhandledRejection — logs at error level for visibility, without forcing
    an exit, matching how the codebase isolates non-fatal errors (Node best practice).

A module-level processHandlersRegistered guard ensures handlers are registered
exactly once per process, regardless of how many server instances start or how
many times start() runs. This also removes the pre-existing
MaxListenersExceededWarning that repeated start() calls produced in tests.

Verification

  • pnpm --filter=@databricks/appkit typecheck — pass
  • vitest run server.test.ts — 30/30 pass (and the MaxListeners warning is gone)
  • vitest run server.integration.test.ts — 8/8 pass
  • biome check on the changed file — clean

This pull request and its description were written by Isaac.

Unhandled async errors previously crashed the Node process silently in
production. Register process-level uncaughtException/unhandledRejection
handlers in the server plugin, in the same place SIGTERM/SIGINT are wired.
uncaughtException logs and runs the existing graceful-shutdown path then
exits non-zero; unhandledRejection logs at error level. A module-wide guard
registers the handlers once per process so repeated start() calls don't
stack listeners.

Co-authored-by: Isaac
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas MarioCadenas requested a review from a team as a code owner June 13, 2026 19:26
…ervers set

Replaces the module-level boolean guard with a static startedServers Set
whose emptiness is the install-once latch: registration stays lazy
(importing the module attaches nothing) and needs no separate mutable
flag. Handlers now shut down every started server rather than only the
first instance to start (the boolean closed over the first instance), and
the per-instance method becomes a static installer.

Co-authored-by: Isaac
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Reverts the started-servers Set over-engineering. A single server runs per
process, so a plain once-flag is sufficient; it only exists to stop repeated
start() in tests from stacking listeners. Also trims verbose comments.

Co-authored-by: Isaac
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant