Skip to content

Guides

JavaScript error handling for AI-generated apps: where errors escape

We threw errors in eight places in a React 19 page and five places in a Node.js server, three runs each. Some crashed the server, some left a button that did nothing, and one hung a request forever.

Andrew Babo6 min read

Share
On this page
JavaScript error handling for AI-generated apps: where errors escape

JavaScript errors escape a React and Node.js app through four doors: render errors with no boundary, handler and promise errors that skip React, unhandled rejections that crash a Node.js server, and work that fails after the response. Each door needs its own few lines of code. This post shows what each door looks like and how to close it.

We tested all four on 26 October 2026: eight failure cases in a React 19 production build running in Chromium and five in a Node.js 22 server, three runs each. Every run gave the same result.

This matters more when an AI coding tool wrote much of the code. Not because the code is worse, but because you did not write the error paths yourself and may not know they exist.

The browser: eight ways a React page can fail#

The test page had one error boundary around a set of buttons, and listened to window error and unhandledrejection itself. Each button failed in a different way.

What threwWho saw itWhat the user saw
Render, inside a boundarythe boundary, plus onCaughtError if setthe fallback
Render, no boundary abovewindow error by default; only onUncaughtError once seta blank page
onClick handlerwindow errornothing changed
setTimeout callbackwindow errornothing changed
Awaited promise in a handlerwindow unhandledrejectionnothing changed
Standalone startTransition, async or syncwindow errornothing changed
useTransition actionthe boundarythe fallback

Two rows deserve a second look. A render error with no boundary above it removed the whole UI, and once the app passed its own onUncaughtError to createRoot, the window never heard about it. And for most errors React left the UI as it was: the user pressed a button and nothing happened. That is the bug report you never get.

The React error boundaries post has the full lab and the setup for each case.

The server: five ways a Node.js process can fail#

Node.js changed its default for unhandled rejections to throw in version 15: “Changed default mode to throw. Previously, a warning was emitted.” We measured what that means for a server with a slow request already in flight.

What saw each of the 13 failures in our React and Node.js labs
Lab 26 Oct 2026 · React 19.2 in Chromium, Node.js 22 · 3 runs per case
What failedNo SDKWith @sentry/node 11
Promise.reject() with no handlerprocess exitsreported, warning printed, process keeps running
Throw inside setTimeoutprocess exitsreported, process still exits
try/catch around that setTimeoutprocess exits, the catch never runsreported, process still exits
Async HTTP handler throws after awaitprocess exits, the in-flight request dies tooreported; that request never gets a response
Background promise rejects after res.end("ok")client got 200, then the process exitsreported; every request answered

Without an SDK, one unawaited rejection in one handler took the server down, along with a request that was doing nothing wrong. The SDK changed that: it registers its own unhandledRejection listener, reports the error and keeps the process alive.

Staying alive is not the same as answering. With the SDK, the request whose handler threw never got a response, and our client gave up after two seconds. The user would see a spinner. Only a try/catch that sends a 500, or your framework's error handler, closes that request.

The try/catch around setTimeout row is the oldest trap in JavaScript. MDN puts it plainly: errors thrown from a later callback “are not catchable by the caller”.

The four doors and what closes each#

The four doors JavaScript errors escape through, and what closes each
From the two labs on 26 Oct 2026

1. Render errors: boundaries and root callbacks#

Wrap each risky part of the page in an error boundary so one broken widget shows a fallback, not a blank page. On React 19, pass a reporting function to all three createRoot options: onCaughtError, onUncaughtError and onRecoverableError. Errors a boundary caught never reach the window, so a tracker that only listens there will not see them.

2. Handlers, timers and promises: window listeners#

These skip React entirely. A browser error tracker listens to window error and unhandledrejection, which MDN describes as firing “when a JavaScript Promise that has no rejection handler is rejected”. Make sure it starts before your app code, and that nothing else replaces window.onerror later.

3. Server handlers: catch, answer, report#

In every request handler that awaits something, catch the error, send a 500 and report it. Frameworks such as Express and Fastify have an error handler for this; use it, and check that it reports. Do not add a bare process.on("unhandledRejection", () => {}) to stop the crashes: it hides every future bug and leaves requests hanging.

4. Work after the response: report from inside#

Background work, waitUntil on Cloudflare, or a void promise after res.end() fails when no one is waiting. Put a .catch() on it that reports the error. Our Cloudflare Workers post found the same gap on the edge: a rejected waitUntil promise still returned 200.

What to search for in code you did not write#

These patterns are worth a quick search in any codebase, especially one that an AI tool generated faster than you could read it:

  • catch {} or catch (e) {} with nothing inside: the error is gone.
  • catch (e) { console.log(e) } on the server: the error reaches a log nobody reads.
  • .then( without a .catch( on the same chain, and void someAsyncCall().
  • async request handlers with no try.
  • throw "message" or throw { ... } instead of throw new Error(...): no stack trace.
  • A new error thrown inside a catch without the original. Pass it along as cause, which keeps “the specific original cause of the error”.

Once errors are reported, make the stack traces readable: a minified frame like app.mjs:6:29 tells you very little. The source maps post shows what Vite's three settings actually produce.

Where Belvador fits#

Belvador's browser and React packages listen to window errors and rejections, ship an error boundary, and provide reactErrorHandler() for the three React 19 root options. The browser SDK guide and the React SDK guide show the setup. On Node.js, Belvador uses the official Sentry SDK with a Belvador DSN, the same SDK we tested here; the Node.js guide covers it. The React and Node.js pages show the whole setup in one place.

Keep reading#

In this topic

All posts

Read next in the docs

See your own production errors this way

Belvador runs on your own Hetzner server: errors, cron jobs, uptime and traces, with your data staying yours.