fix(express)!: require Express 5 and add router error middleware#89
Merged
Conversation
Under Express 4 a rejected handler promise was never routed anywhere, so an upstream failure left the request hanging until the client timed out. Express 5 forwards rejected handler promises to error middleware, which terminates those requests properly. The Express built-in error handler answers with an HTML stack trace including absolute server paths whenever NODE_ENV is not production, so the router now registers its own error middleware that returns a JSON 500 and logs the cause server side. BREAKING CHANGE: express and @types/express peer ranges are now >=5.0.0.
This was referenced Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #73 (the last remaining item, item 4). The other four items landed in #88.
Requires Express 5 rather than adding an async wrapper to keep Express 4 working. Taking the break now while the package is pre-1.0, so adopters are not asked to absorb it later at a higher cost.
Why the peer range and not a wrapper
Every route handler already returns its promise to Express. Registrations use concise-body arrows (
(req, res) => handler(...)), and the single block-body arrow isasync, so nothing is actually discarded. Express 5 forwards those rejections to error middleware; Express 4 does not.Reproduced with the same code and input (upstream
fetchrejects), varying only the Express version:ECONNABORTED) after 2sSo the hang was purely an Express 4 compatibility issue, and moving the floor to 5 resolves it without new runtime code.
Migration cost
Scanned for Express 4 to 5 breakage in this package and found none: no wildcard, optional, or regex route paths (path-to-regexp v8 is the usual source of breakage), no
req.param(),res.send(status),res.json(obj, status),app.del, orredirect('back').res.clearCookiepasses onlydomainandpath.buildProxyQueryStringhandles only strings and string arrays, so the simple query parser default is a no-op here.CI already ran entirely on Express 5, so this package needed no changes to support it.
Error middleware
Requiring Express 5 stops the hang but hands the request to Express's built-in error handler, which answers with an HTML stack trace including absolute server paths whenever
NODE_ENVis notproduction. That is a poor default for an auth package and also breaks the JSON contract every other route follows.The router now registers its own error middleware:
500with a JSON{ "error": "internal_error" }body, cause logged server side, and it delegates tonext(err)when headers were already sent.Tests
Added
packages/express/tests/routeErrorHandling.test.js(3 tests), mirroring the existing suites.Verified they are not vacuous by removing the middleware and re-running. The "answers 500" test passes either way, since Express 5 alone returns 500. The JSON-body/no-leak and server-side-logging tests both fail without it, which is the correct split.
Verification
pnpm buildclean.pnpm testat the workspace root: core 15 suites / 60 tests, express 20 suites / 76 tests, all passing.One transient failure in
tests/cookieSecurity.test.jsappeared in a single run on a loaded machine and did not reproduce. It passes in isolation, passes in the full suite across 6 subsequent clean runs, and--detectOpenHandleson the new test file is clean. Noting it rather than omitting it. That file mutatesprocess.env.NODE_ENV, which is worth keeping an eye on, though it is pre-existing and unrelated to this change.Changeset is a minor bump, which is the pre-1.0 convention for a breaking change.