oxc-project/oxc · error · OxcDiagnostic
Express endpoint handler for `{endpoint}` should not be `asy
Error message
Express endpoint handler for `{endpoint}` should not be `async`. What it means
Oxlint rule `oxc/no_async_endpoint_handlers` flags `async` functions passed as Express route handlers. Express 4 does not catch promise rejections from handlers: a rejected async handler becomes an unhandled promise rejection, skipping the error middleware (`next(err)` is never called) and potentially crashing Node on `unhandledRejection`. The message interpolates the endpoint path (or the generic form when the path is not a static string), and the note says to disable the rule on Express 5, which handles rejections natively.
Source
Thrown at crates/oxc_linter/src/rules/oxc/no_async_endpoint_handlers.rs:78
// Shouldn't happen, since separate declaration/registration requires an
// identifier to be bound
(Some(span), None) => &[
async_span.label("Async handler is declared here"),
span.primary_label(registered_label),
],
// `app.get('/foo', async function foo(req, res) {});`
(None, Some(name)) => &[async_span.label(format!("Async handler '{name}' is used here"))],
// `app.get('/foo', async (req, res) => {});`
(None, None) => &[async_span.label("Async handler is used here")],
};
let warning = endpoint.map_or_else(
|| "Express endpoint handler should not be `async`.".to_string(),
|endpoint| format!("Express endpoint handler for `{endpoint}` should not be `async`."),
);
OxcDiagnostic::warn(warning)
.with_labels(labels.iter().cloned())
.with_help(
"Wrap the async handler and forward errors to `next()` (e.g. `(req, res, next) => Promise.resolve(handler(req, res, next)).catch(next)`).\nExpress does not automatically handle rejected promises from async handlers, which results in unhandled promise rejections and server crashes.",
).with_note(
"If you're on Express 5, disable this rule. To allow specific functions, add their names to `allowedNames`."
)
}
declare_oxc_lint!(
/// ### What it does
///
/// Disallows the use of `async` functions as Express endpoint handlers.
///
/// ### Why is this bad?
///
/// Before v5, Express will not automatically handle Promise rejections from
/// handler functions with your application's error handler. You must
/// instead explicitly pass the rejected promise to `next()`.View on GitHub (pinned to e1e7af627c)
Solutions
- Wrap handlers so rejections reach `next`: `const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next)` and register `wrap(asyncHandler)`
- Or use a library: `express-async-handler` or the `express-async-errors` shim imported once at startup
- Use try/catch inside the handler and call `next(err)` in the catch block
- Add the handler's name to the `allowedNames` option for deliberate exceptions
- On Express 5, disable the rule: `"oxc/no_async_endpoint_handlers": "off"`
Example fix
// before
app.get('/users', async (req, res) => {
const users = await db.all('SELECT * FROM users');
res.json(users);
});
// after
const wrap = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users', wrap(async (req, res) => {
const users = await db.all('SELECT * FROM users');
res.json(users);
})); Defensive patterns
Strategy: try-catch
Try / catch
const wrap = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users', wrap(async (req, res) => {
res.json(await db.all());
}));
// all handler rejections are forwarded to the Express error middleware Prevention
- Adopt one wrapper convention (express-async-handler or a shared wrap util) project-wide and ban raw async handlers in code review
- Add a process-level unhandledRejection logger so any unwrapped handler is visible in production
- On Express 5, remove the wrapper convention and disable the rule to avoid double-handling confusion
When it happens
Trigger: `app.get('/foo', async (req, res) => { ... })`, `router.post('/bar', async function handler(req, res) { ... })`, or the same as middleware-array entries — i.e. any async function value passed where Express expects a handler, with the route path known at lint time. Config `allowedNames` exempts specifically named functions.
Common situations: Express 4 apps with async/await DB or fetch calls inside handlers where errors vanish past the error middleware; Node 15+ processes exiting on unhandled rejections; teams upgrading to Express 5 but keeping the rule enabled.
Related errors
- Promise executor functions should not be `async`.
- Expected throw instead of Promise.reject
- Prefer `catch` to `then(a, b)` or `then(null, b)`
- Async assertions must be awaited.
- Promises which return async assertions must be awaited.
AI-assisted analysis of oxc-project/oxc@e1e7af627c (2026-08-20).
Data as JSON: /api/errors/19569d680385b6f1.
Report an issue: GitHub.