/changelogs/express/express-4-to-5
Express 4 to 5 migration: breaking changes
verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
- tool
- Express
- versions
- 4.x → 5.0
Express 5.0.0 was published on npm in September 2024, about ten years after 4.0; since 5.1.0 (March 2025) it is the default latest tag. The changes most projects hit are the stricter route syntax (app.get('*') throws Missing parameter name), removed deprecated method signatures, and automatic handling of rejected promises. The Express team publishes codemods: npx @expressjs/codemod upgrade.
Seen in practice: async errors and removed signatures
The same app under Express 4 and Express 5. In 4 the rejected promise is not handled by Express (the request would hang); in 5 it reaches the error handler. res.send(404) sets the status in 4; in 5 the number is sent as the response body with status 200:
cat > app.js <<'EOF'
const express = require("express");
const app = express();
app.get("/async", async () => { throw new Error("db down"); });
app.get("/old", (req, res) => res.send(404));
app.use((err, req, res, next) => res.status(500).send("handled: " + err.message));
process.on("unhandledRejection", (e) => console.log(" unhandledRejection:", e.message));
const server = app.listen(4403, "127.0.0.1", async () => {
for (const u of ["/async", "/old"]) {
const ctl = AbortSignal.timeout(1500);
try {
const r = await fetch("http://127.0.0.1:4403" + u, { signal: ctl });
console.log(" ", u, r.status, (await r.text()).split("\n")[0].slice(0, 60));
} catch { console.log(" ", u, "no response (timed out)"); }
}
server.close(); process.exit(0);
});
EOF
npm init -y >/dev/null
for v in 4 5; do
npm install --silent --no-audit --no-fund express@$v >/dev/null 2>&1
echo "--- express $v"
node app.js 2>&1 | grep -v "deprecated"
doneOutput
--- express 4 unhandledRejection: db down /async no response (timed out) /old 404 Not Found --- express 5 /async 500 handled: db down /old 200 404
Breaking changes and fixes
| Express 4 | Express 5 |
|---|---|
| app.get('*'), '/api/*' | '/{*splat}', '/api/*splat' (named wildcards) |
| '/users/:id?' | '/users{/:id}' |
| res.send(404) sets the status | res.send(404) sends the body 404 with status 200; use res.sendStatus(404) |
| res.json(obj, 201) | res.status(201).json(obj) |
| res.redirect('/x', 301) | res.redirect(301, '/x') |
| app.del() | app.delete() |
| req.param('id') | req.params.id, req.body.id or req.query.id |
| async errors needed try/catch + next(err) | rejected promises go to the error handler automatically |
| req.body = {} without parser | req.body is undefined without a body parser |
| req.query writable, extended parser | read-only getter; set "query parser" to "extended" for nested objects |
Upgrade steps
- Upgrade Node.js to 18 or later.
- Run
npm install express@5and the codemods (npx @expressjs/codemod upgrade). - Search for
'*'and:param?in routes and rewrite them with the new syntax. - Remove
try/catchblocks that only forwarded errors tonext(err)in async handlers, if you want; they still work. - Check middleware packages for Express 5 support (most popular ones work unchanged).
Sources
Related pages
errors · expressExpress 5: TypeError: Missing parameter name at index 1: *Express 5 uses path-to-regexp 8, so app.get('*') throws Missing parameter name. Name the wildcard, as in /*splat or /{*splat}, or use app.use() for a fallback.glossary · expressWhat is middleware in Express?Express middleware is a function (req, res, next) that runs in registration order for matching requests. It can end the response or call next() to continue.errors · expressExpress: Error: listen EADDRINUSE: address already in use :::3000Node.js and Express fail with listen EADDRINUSE when another process already uses the port. Find it with lsof, ss or netstat, stop it, or listen on a free port.