We scanned 6,283 TypeScript repositories. Here's what we found.
By Nark Team
We scanned 6,283 open source TypeScript repositories and found 71,140 call sites where errors go unhandled. 40% of projects had at least one. For the packages that make up the standard TypeScript backend stack — axios, Prisma, zod, jsonwebtoken — the violation rates ranged from 73% to 98%.
Methodology
We selected TypeScript repositories on GitHub with more than 100 stars, at least one commit in the past six months, and TypeScript as a primary language. Each repository was scanned with Nark against its root tsconfig.json.
Nark ships with Profiles for 160+ npm packages. Each Profile documents what the package can throw and when — network errors, bad status codes, parse failures, constraint violations, verification errors. For every async call site in the TypeScript source that matches a profiled package, Nark checks whether the caller handles the documented failure modes. A "violation" means the call site has no error handling for a failure the package is documented to produce. A bare await axios.get(url) with no surrounding try-catch is a violation. The same call inside a try-catch with axios.isAxiosError() handling is not.
6,283 repositories scanned. 96,679 call sites evaluated. 2,487 repositories (40%) with at least one unhandled error path.
axios — 92% of repos have at least one unguarded call
409 repos use axios. 378 have violations. 4,132 total.
Axios throws an AxiosError on any non-2xx response by default. A 401, 429, or 503 from a third-party API becomes a thrown exception. Without a try-catch, that exception propagates unhandled — a 500 in a route handler, a silent crash in a background job.
botpress/botpress had 134 unguarded axios calls across 35 integrations. One example from the Vonage messaging integration:
// Crashes on 401 (expired credentials), 429 (rate limit), or network failure
const response = await axios.post(
'https://api.nexmo.com/v1/messages',
payload,
{ auth: { username: ctx.configuration.apiKey, password: ctx.configuration.apiSecret } }
)
await ack({ tags: { id: response.data.message_uuid } })
When Vonage returns a non-2xx status, the raw AxiosError crashes the handler and surfaces a stack trace instead of a meaningful error. The fix:
try {
const response = await axios.post('https://api.nexmo.com/v1/messages', payload, { auth })
await ack({ tags: { id: response.data.message_uuid } })
} catch (error) {
if (axios.isAxiosError(error)) {
const status = error.response?.status ?? 'network error'
throw new RuntimeError(`Failed to send Vonage message (${status}): ${error.message}`)
}
throw error
}
92% means this is the default state. Most TypeScript projects that use axios have at least one call that crashes when the remote server misbehaves.
zod — the validation library that doesn't validate its own output
239 repos use zod. 174 have violations (73%). 1,808 total.
Zod's parse() throws a ZodError when input doesn't match the schema. If you don't catch it in an API route, malformed client input triggers a 500 instead of a 400:
// ZodError becomes an unhandled 500
const { userId } = schema.parse(req.query)
// Invalid input returns a 400 with structured error details
const result = schema.safeParse(req.query)
if (!result.success) return res.status(400).json({ error: result.error })
const { userId } = result.data
civitai/civitai had approximately 100 unguarded schema.parse() calls across public endpoints, webhook handlers, and moderation tools. Any malformed request produces a generic 500 that shows up in error monitoring as a server bug rather than a client input problem.
The irony is that zod exists to make input validation safe and ergonomic. It does — but only if you use safeParse(). The 73% rate suggests most projects reach for parse() by default and don't revisit it.
@prisma/client — 98%
49 repos use Prisma. 48 have violations. 828 total.
The highest violation rate in our scan. One repo out of 49 had complete error handling coverage. The other 48 had at least one database call that propagates an unhandled rejection on failure.
Prisma throws for connection errors, unique constraint violations, record-not-found, and query timeouts. Each one carries structured metadata — error codes, target fields, constraint names — which is lost if the error bubbles up unhandled:
// Unique constraint violation → unhandled PrismaClientKnownRequestError → 500
const user = await prisma.user.create({
data: { email: input.email, name: input.name }
})
// Catches the specific constraint error and returns a meaningful response
try {
const user = await prisma.user.create({
data: { email: input.email, name: input.name }
})
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
return res.status(409).json({ error: 'A user with this email already exists' })
}
throw error
}
A connection pool error at peak traffic crashes every in-flight request. A unique constraint violation on signup returns a stack trace instead of "email already taken." A timeout during a bulk operation crashes a background job without logging what failed. The 98% rate says this is happening in nearly every TypeScript project with a database.
jsonwebtoken — verification, not decoding
129 repos use jsonwebtoken. 115 have violations (89%). 310 total.
The dominant violation pattern: calling jwt.decode() when jwt.verify() is needed. decode() base64-decodes the payload and returns it without checking the signature. Anyone can construct a JWT with arbitrary claims, and decode() will return them as if they were legitimate. verify() checks the signature first.
NangoHQ/nango used jwt.decode() in several OAuth post-connection hooks to extract tenant IDs and user identifiers from access tokens — claims that influence downstream behavior — without signature verification.
Context matters here. For Microsoft and Xero access tokens, Microsoft's own documentation states that access tokens are opaque to clients and should not be validated by the resource consumer. For those providers, decode() is the correct approach because the token arrived via a server-to-server auth-code exchange over TLS. But the default should be verify(), with decode() as a deliberate exception when the token's provenance is already established by other means.
undici — the largest single count
1,527 repos. 1,408 with violations. 12,051 total.
Most developers using Node.js 18+ don't realize they're using undici — it powers the built-in fetch(). The dominant violation is calling response.json() without a try-catch. When an upstream service degrades and returns an HTML error page instead of JSON, response.json() throws a SyntaxError that crashes the caller. Developers often remember to check response.ok for HTTP status errors but forget that the body parse itself can fail.
@anthropic-ai/sdk — the contrast
34 repos use @anthropic-ai/sdk. 18 have violations (53%). 33 total.
53% is still more than half, but compared to axios (92%) or Prisma (98%), Anthropic SDK users handle errors noticeably more often. The sample size is small — 34 repos — so the rate could shift as adoption grows. But it's worth noting that even with a relatively new, well-documented SDK, more than half of the repos we scanned have at least one unguarded inference call that crashes on a rate limit or network timeout.
The pattern that surprised us
getsentry/sentry-javascript appeared in our top-30 most-violated repositories, with 218 errors and 16 warnings. Sentry builds error monitoring. Their JavaScript SDK is instrumented into millions of production applications to catch unhandled exceptions. Their own repository has 218 call sites with missing error handling.
We also found violations in TanStack/tanstack.com, mantinedev/mantine, NangoHQ/nango, and civitai/civitai. These are not neglected repos. They have active maintainers, CI pipelines, code review, and in many cases comprehensive test suites.
That's the point. Error handling gaps are not a function of carelessness. They accumulate through the normal course of development — the happy path gets tested, the error paths get deferred, and code review focuses on business logic rather than whether every async call site handles every failure mode the underlying package can produce. No linter catches this. No test suite covers it unless someone deliberately writes failure-mode tests for every external dependency. The gap between "works in tests" and "handles every documented failure" is where 71,140 violations live.
What this means
These findings are not from obscure hobby projects. The packages involved — axios, Prisma, zod, jsonwebtoken — are the core of TypeScript backend development. The failure modes — unhandled network errors, uncaught validation throws, unverified JWT claims — are production incidents that happen regularly.
The violations are not subtle. They are call sites with no error handling at all, in code paths exercised by real users. They survive code review because reviewers check logic, not async error coverage. They survive testing because test suites model success. They surface when an upstream dependency fails — which, in production, is a matter of when, not if.
We built Nark to find these patterns. It checks every async call site in your TypeScript codebase against Profiles for 160+ npm packages — including axios, prisma, stripe, and redis — each documenting what the package throws and what correct handling looks like.
npx nark
Open source. AGPL-3.0. github.com/nark-sh/nark