asyncdrop
v0.1.0
Published
Flags Express async route handlers with no try/catch around their awaited calls, in a codebase with no centralized async-error-catching middleware — a rejected promise there becomes a silent hang or crash, not a forwarded error.
Maintainers
Readme
asyncdrop
Static analyzer CLI that flags an Express async (req, res) => { ... }
route handler that awaits without a surrounding try/catch, in a
codebase with no centralized async-error-catching protection — no
express-async-errors import, no catch-all wrapper (asyncHandler/
catchAsync/...), and not already on Express 5.
// Works every time in dev: the DB call always resolves. The day it
// rejects — a dropped connection, a timeout, a bad id — this becomes
// an unhandled promise rejection on Express 4, not a forwarded error.
// No error handler ever runs, and the request can hang forever with no
// response ever sent.
app.get("/users/:id", async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user);
});See DETAILS.md for the full gap analysis, the Express-4-vs-5 behavioral difference this tool has to account for to avoid false positives, and an honest list of what it can't see.
Install
npm install -g asyncdropUsage
npx asyncdrop check src
npx asyncdrop check src/routes.tsExample output:
src/routes.ts:13 [GET /users/:id] async route handler awaits without a surrounding try/catch — a rejected promise here becomes an unhandled rejection, not a forwarded error, unless the codebase is on Express 5 or uses express-async-errors/a catch-all wrapper
asyncdrop: found 1 unguarded async handler finding(s) across 1 handler(s) scanned.Exit codes: 0 clean (or skipped — see below), 1 an unguarded async
handler was found, 2 usage error. Full flag/output reference in
docs/USAGE.md.
What it detects
- A route registered via
app.get/post/put/patch/delete/all(...)orrouter.get/post/...(...)whose handler isasyncand contains at least oneawaitthat is not inside atryblock. - Handlers wrapped in a recognizable catch-all utility
(
asyncHandler(...),catchAsync(...),wrapAsync(...), ...) are treated as protected — the wrapper is doing the forwarding job for them. - The whole project is skipped (nothing flagged) when:
- the nearest
package.jsonpinsexpressto major version 5 (its router already forwards rejected promises to error middleware automatically — see DETAILS.md's Express 5 note), or - an
express-async-errorsimport/requireis found anywhere in the scanned files (it patches Express 4's router to do the same thing).
- the nearest
Parsing is done directly with the TypeScript Compiler API, so
asyncdrop understands real TypeScript/JavaScript syntax rather than
regexing source text — it never executes the code it scans.
What this is not
- Not a general async/await linter —
no-floating-promises(fromtypescript-eslint) and similar rules catch a broader class of unhandled-rejection shapes anywhere in a codebase, not specifically "route handler with no centralized catch path."asyncdropis scoped tightly to the Express/Fastify route-registration call-site pattern. - Not a check on a bare 4-arg error-handling middleware being registered — see DETAILS.md for why that alone is not sufficient protection on Express 4 and isn't treated as one.
- Not a runtime/observability tool — it never imports, requires, or executes the scanned code; everything is read as source text.
License
MIT © 2026 Jay
