Express 5 Migration Guide: What Changed and What Breaks
Every Express 5 breaking change you are likely to hit, tested against Express 5.3 with real error messages: async errors, new route syntax, req.body, removed methods and a silent res.send trap.

Mohit Malaya Nandy
Software Engineer & Product Builder

On this page
Express 5 is now the version you get with npm install express. Most apps upgrade with only a few changes, but a handful of breaking changes cause confusing errors, and some fail silently. Many tutorials online still show Express 4 code that no longer works.
This guide covers every change you are likely to hit, with before-and-after code. I tested the main ones against Express 5.3 and included the exact error messages, so you can recognise them. It follows the official migration guide.
Before you start#
- Node.js 18 or newer is required.
- Upgrade with
npm install express@5. - Make sure you have tests or a list of routes to try by hand afterwards.
Then run the official codemods, which fix many renamed and removed APIs automatically:
npx codemod@latest @expressjs/v5-migration-recipeCommit before running them so you can review the diff. Codemods do not fix route patterns or behaviour changes, which are covered below.
1. Async errors are handled for you (the best change)#
In Express 4, an error thrown in an async handler was not caught, so you wrapped every route in try/catch or used a helper like express-async-handler. In Express 5, rejected promises go straight to your error handler (error handling docs):
// Express 4
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (err) {
next(err);
}
});
// Express 5
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id); // errors reach the error handler
res.json(user);
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Something went wrong' });
});In my test, a handler that threw new Error('boom from async') was caught by the error handler and returned a 500, with no try/catch anywhere. You can now delete express-async-handler and similar wrappers.
2. Route path syntax changed#
Express 5 uses a new version of path-to-regexp. Old patterns now throw a PathError when the app starts:
app.get('/*', …)
PathError: Missing parameter name at index 2: /*
app.get('/:file.:ext?', …)
Unexpected ? at index 11: /:file.:ext?| Express 4 | Express 5 | Notes |
|---|---|---|
| /* | /*splat | Wildcards must have a name |
| /* (including the root /) | /{*splat} | Braces make it optional, so / matches too |
| /:file.:ext? | /:file{.:ext} | ? is gone; wrap optional parts in braces |
| /[discussion|page]/:slug | ['/discussion/:slug', '/page/:slug'] | Regex syntax is gone; use an array of paths |
| Characters ( ) [ ] ? + ! | Escape with \ | They are now reserved |
Wildcard values are now arrays of path segments:
app.get('/files/*splat', (req, res) => {
res.json({ splat: req.params.splat });
});
// GET /files/a/b/c.txt -> { "splat": ["a", "b", "c.txt"] }
// Need a string? req.params.splat.join('/')3. req.body is undefined until parsed#
In Express 4, req.body was an empty object even when nothing was parsed. In Express 5 it is undefined. My test confirmed it: a POST without a body gave req.body === undefined. Destructuring it directly then crashes:
// Throws "Cannot destructure property 'email' of 'req.body' as it is undefined"
const { email, password } = req.body;
// Safe in Express 5
const { email, password } = req.body ?? {};Also note that express.urlencoded() now defaults to extended: false. Pass { extended: true } if you relied on nested form fields like user[name].
4. Removed and renamed methods#
| Express 4 | Express 5 |
|---|---|
| app.del() | app.delete() |
| req.param('id') | req.params.id, req.body.id or req.query.id |
| res.json(obj, 404) | res.status(404).json(obj) |
| res.send(body, 404) | res.status(404).send(body) |
| res.send(404) | res.sendStatus(404) |
| res.redirect(url, 301) | res.redirect(301, url) |
| res.redirect('back') | res.redirect(req.get('Referrer') || '/') |
| res.sendfile() | res.sendFile() |
| req.acceptsCharset() and similar | req.acceptsCharsets() (plural) |
| express.static.mime | The mime-types package |
5. Smaller behaviour changes#
res.status()is strict: invalid codes throw. My test ofres.status(1000)gave "Invalid status code: 1000. Status code must be greater than 99 and less than 1000."req.queryis read-only (a getter), and the default parser is now"simple", so nested query strings like?filter[status]=openare no longer turned into objects by default.express.staticignores dotfiles by default, including folders like/.well-known. Useexpress.static('public', { dotfiles: 'allow' })if you serve those files.req.hostnow includes the port number.res.clearCookie()ignoresmaxAgeandexpires.app.listen()passes errors likeEADDRINUSEto its callback instead of throwing.- MIME types:
.jsfiles are now served astext/javascript. - Brotli: the body parsers can now decompress
br-encoded request bodies.
Migration checklist#
- Upgrade Node.js to 18 or newer.
- Commit, then run the official codemods and review the diff.
- Search for
'*',?and regex in route paths, and update them. Start the app; any remaining bad pattern throws aPathErrorat startup. - Replace
res.send(<number>)withres.sendStatus(). - Guard
req.bodywith?? {}, and checkexpress.urlencodedoptions. - Remove async wrappers and try/catch blocks that only call next(err).
- Check query string parsing, static dotfiles and cookie clearing if you depend on them.
- Run your tests, and try each route with good and bad input.
Frequently asked questions#
What Node.js version does Express 5 need?
Express 5 requires Node.js 18 or newer.
Why does app.get('/*') throw an error in Express 5?
Express 5 uses a newer path-to-regexp, where wildcards must be named. Replace '/*' with '/*splat', or '/{*splat}' if it should also match the root path.
Do I still need try/catch or express-async-handler in Express 5?
No. In Express 5, if an async route handler or middleware throws or returns a rejected promise, the error is passed to your error-handling middleware automatically.
Is there an automatic migration tool for Express 5?
Yes. The Express team publishes codemods you can run with npx codemod@latest @expressjs/v5-migration-recipe. They fix many renamed and removed APIs, but you still need to review route patterns and behaviour changes yourself.
Why is req.body undefined in Express 5?
In Express 5, req.body is undefined until a body parser has parsed the request, instead of defaulting to an empty object. Use express.json() and guard with req.body ?? {} before destructuring.
Official references#
- Migrating to Express 5, Express documentation
- Error handling and Routing, Express documentation
- path-to-regexp
- Security best practices, Express documentation
Found this useful? You can support my writing on GitHub Sponsors or with a coffee.


