← Back to Blog

Why TypeScript Needs a Dependency Error Checker (And ESLint Isn't Enough)

By Nark Team

TypeScript's type system prevents you from passing the wrong type to a function. ESLint prevents you from forgetting to await a promise. But neither tool knows what your npm dependencies throw at runtime — and that gap causes production crashes that no existing static analysis tool will warn you about.

Quick Answer: TypeScript and ESLint check your code's structure. They don't know what axios.get(), prisma.user.create(), or stripe.charges.create() can throw. A dependency error checker like Nark fills that gap by scanning your code against profiles for 160+ npm packages. Run it: npx nark --tsconfig ./tsconfig.json


What TypeScript Guarantees (And What It Doesn't)

TypeScript's type system is remarkable at what it does. It catches:

  • Wrong argument types (string where number is expected)
  • Missing required properties on objects
  • Calling methods that don't exist on a type
  • Null/undefined dereferences when strictNullChecks is enabled

What TypeScript cannot tell you is whether you handled all the errors a function can throw. TypeScript's type system does not track exceptions. The throws annotation doesn't exist in TypeScript. A function typed as Promise<User> might actually throw PrismaClientKnownRequestError, PrismaClientInitializationError, PrismaClientRustPanicError, or PrismaClientUnknownRequestError — and TypeScript tells you nothing about any of these.

// TypeScript: PASSES — the return type matches
async function createUser(email: string): Promise<User> {
  return await prisma.user.create({
    data: { email },
  });
  // But this throws PrismaClientKnownRequestError with code P2002
  // when email already exists. TypeScript doesn't know. Your code doesn't handle it.
}

This function passes TypeScript compilation with zero errors. At runtime, duplicate emails cause an unhandled exception that propagates up, bypasses your error handling middleware if not specifically caught, and returns a 500 to the user instead of "Email already taken."


What ESLint Guarantees (And What It Doesn't)

ESLint's @typescript-eslint/no-floating-promises rule catches the most obvious case — an async call with no error handling at all:

// ESLint no-floating-promises: CATCHES this
stripe.charges.create({ amount: 1000, currency: 'usd', source: token });

But add a try-catch — any try-catch — and ESLint is satisfied:

// ESLint no-floating-promises: PASSES
// nark: FAILS (StripeCardError not handled)
try {
  const charge = await stripe.charges.create({
    amount: 1000,
    currency: 'usd',
    source: token,
  });
  return charge;
} catch (error) {
  // This generic catch doesn't distinguish:
  // - Card declined (StripeCardError) → show user a message
  // - Rate limit (StripeRateLimitError) → retry with backoff
  // - Invalid request (StripeInvalidRequestError) → bug in our code
  // - Network error → retry
  throw new Error('Payment failed'); // all cases get the same response
}

ESLint sees a try-catch and considers the error handled. The actual runtime behavior — that card declines, rate limits, and network errors all require different responses — is invisible to ESLint.


The Gap: npm Package Runtime Behavior

When you install an npm package, you get TypeScript type definitions for its API surface. You do not get a machine-readable specification of:

  • What error classes the package throws in which situations
  • Which error properties are always present versus conditionally present
  • What the correct handling looks like for each error category
  • What happens if you miss a specific error type

This information exists in documentation, changelogs, GitHub issues, and production incident post-mortems — but it's not encoded anywhere your static analysis tools can read.

Real examples of what gets missed

axios: Throws AxiosError for all failures. But AxiosError has three distinct shapes:

  • HTTP error: error.response is defined, error.request is defined
  • Network error: error.response is undefined, error.request is defined
  • Setup error: both are undefined

Code that accesses error.response.status without checking error.response existence crashes with TypeError: Cannot read properties of undefined on network failures. TypeScript doesn't track this. ESLint doesn't track this. It only crashes in production.

Prisma: Throws multiple distinct error classes. PrismaClientKnownRequestError carries an error code (P2002 = unique constraint, P2025 = record not found). Generic catch (error) { throw error } doesn't give users meaningful responses. TypeScript says the function returns User. It doesn't say "may throw P2002."

Redis: A connection to Redis that never registers an .on('error') handler will crash the entire Node.js process when the Redis server becomes unavailable. The EventEmitter error event propagation rule requires an error handler or the process terminates. TypeScript's type system has no concept of this behavior.

Stripe: A charge creation that catches Error but not StripeCardError specifically will log a card decline as an unhandled error, fail to show the customer a meaningful message, and potentially retry in a loop without checking whether the card was simply declined versus temporarily unavailable.


Why This Category of Bug Is Growing

Three trends are making this problem worse:

1. AI-generated code skips error handling. When you ask an AI assistant to write an axios request, it writes the happy path. The generated code calls axios.get() inside async functions, often with a generic try-catch or none at all. The AI knows axios's API surface (from TypeScript types) but not its runtime error behavior. Every AI-generated API integration needs to be audited for missing error handling.

2. npm package APIs change. When Prisma releases a new version, error classes and codes can change. When Redis adds new error types for cluster operations, your catch blocks from 2022 may be missing new cases from 2025. There's no automated signal when a package update invalidates your error handling code.

3. Projects grow beyond what code review can audit. A mid-size TypeScript project might have hundreds of API calls across dozens of files. Manually checking that each one handles all error cases from its respective npm package is not feasible during code review. The reviewer checks that there's a try-catch, not that the try-catch is correct.


What a Dependency Error Checker Does

A dependency error checker like Nark bridges the gap between what TypeScript knows (API types) and what your code actually needs to handle (runtime error cases).

It works by:

  1. Parsing your code with the TypeScript compiler — same AST TypeScript uses, so it understands imports, types, class instances, and all TypeScript-specific syntax
  2. Identifying calls to profiled npm packages — finds axios.get(), prisma.user.create(), stripe.charges.create(), and their equivalents on class instances
  3. Checking each call against the package's profile — a machine-readable YAML specification of what the package throws, what error types exist, and what handling is required
  4. Reporting violations — each violation identifies the file, line, function, and the specific postcondition that's not being met
$ npx nark --tsconfig ./tsconfig.json

src/services/payment.ts:47 — stripe
  [ERROR] stripe.charges.create() — StripeCardError not handled
  Stripe throws StripeCardError when a card is declined. This is a distinct error
  class from StripeRateLimitError and requires user-facing handling.

src/api/users.ts:23 — @prisma/client
  [ERROR] prisma.user.create() — P2002 unique constraint not handled
  PrismaClientKnownRequestError with code P2002 is thrown when a unique
  constraint is violated (e.g., duplicate email). Handle separately from
  other Prisma errors to return a user-facing message.

src/lib/cache.ts:12 — ioredis
  [ERROR] Redis client missing .on('error') handler
  Redis clients without an error handler will crash the Node.js process on
  connection failure. Register an error handler before making any Redis calls.

Each violation is deterministic — the same call without the same error handling always produces the same violation. There's no probability threshold, no AI guess. Pass or fail.


Adding Nark to Your TypeScript Project

Nark works on any TypeScript project with a tsconfig.json. No configuration required to start.

# Run once to see violations
npx nark --tsconfig ./tsconfig.json

# Add to CI to catch new violations on every PR
# .github/workflows/quality.yml

For CI integration:

- name: nark — dependency error handling
  run: npx nark --tsconfig tsconfig.json

Nark exits with code 0 (clean) or 1 (violations found), so it integrates naturally into any CI pipeline that fails on non-zero exit codes.


The Right Mental Model

Think of your code quality toolchain as three layers:

  1. TypeScript compiler — catches type mismatches, missing properties, structural errors
  2. ESLint — catches style violations, language anti-patterns, completely unhandled promises
  3. Nark — catches incorrectly handled promises for specific npm packages

Layer 1 is already in your tsc or next build step. Layer 2 is already in your CI if you run ESLint. Layer 3 is the missing piece for most TypeScript projects.

The cost of not having layer 3 is the class of production incidents that TypeScript and ESLint cannot prevent: unhandled card declines, silent data loss on Prisma unique constraints, process crashes on Redis connection failure, and retry storms when OpenAI returns a rate limit error you're treating as a permanent failure.


Frequently Asked Questions

Can I catch these errors with better TypeScript types?

TypeScript 4.9+ added satisfies and better inference, but the language still doesn't track exceptions in the type system. Even with strict mode and noUncheckedIndexedAccess, TypeScript cannot tell you what an async function throws. This is by design — adding checked exceptions to TypeScript would break compatibility with the JavaScript ecosystem.

Can ESLint plugins replace Nark?

You could write a custom ESLint rule for each npm package. But you'd need to author, test, and maintain rules for every package, update them when packages add new error types, and handle all the TypeScript-specific call patterns (class instances, destructured methods, re-exported functions). Nark's profile corpus encodes this work for 160+ packages.

How is Nark different from dependency vulnerability scanners like npm audit?

npm audit finds packages with known security vulnerabilities in the package database. Nark finds missing error handling in your code for packages you're already using correctly. They check completely different things.


Try It Now

npx nark --tsconfig ./tsconfig.json

Nark takes about 30 seconds on a typical project. No signup, no config, no AI. Uses the TypeScript compiler directly to find the error handling gaps that TypeScript and ESLint can't see.