← Back to Blog

What Static Analysis Tools Check for Missing Error Handling in TypeScript?

By Nark Team

The main tools for catching missing error handling in TypeScript are ESLint (with @typescript-eslint/no-floating-promises), the TypeScript compiler in strict mode, Semgrep with custom rules, and Nark, which scans your AST against 160+ package-specific Nark profiles. ESLint catches unhandled promises (also known as unhandled promise rejections) but not missing try-catch around specific package calls. TypeScript strict mode catches type errors but has no concept of runtime throwing behavior. These tools help you detect errors before runtime, but Nark is the only tool that knows, for example, that axios.get() can throw AxiosError and requires a try-catch.

Quick Answer: ESLint catches floating promises, TypeScript strict catches type errors, and Semgrep lets you write custom rules. For package-specific error handling (like "did you wrap prisma.user.create() in a try-catch?"), use Nark: npx nark --tsconfig ./tsconfig.json


How Does ESLint Catch Unhandled Async Errors?

ESLint with @typescript-eslint provides the most commonly used rule for async error handling: no-floating-promises. This rule flags any Promise that isn't awaited, .then()'d, .catch()'d, or explicitly voided. A floating promise is one of the most common sources of unhandled promise rejections in TypeScript applications.

// eslint.config.js
import tseslint from 'typescript-eslint';

export default tseslint.config({
  rules: {
    '@typescript-eslint/no-floating-promises': 'error',
  },
});

Here is what it catches:

import axios from 'axios';

// ESLint flags this: "Promises must be awaited, end with a call
// to .catch, or end with a call to .then with a rejection handler."
axios.get('https://api.example.com/users');

// ESLint is fine with this (promise is awaited):
await axios.get('https://api.example.com/users');

The problem: ESLint stops caring once you await the promise. It does not check whether the await is inside a try-catch. This code passes ESLint with zero warnings:

import axios from 'axios';

async function getUsers() {
  // No ESLint error. No try-catch. If the API returns 500,
  // this throws an unhandled AxiosError at runtime.
  const response = await axios.get('https://api.example.com/users');
  return response.data;
}

ESLint also has no-unsafe-finally and no-throw-literal, which help with error handling hygiene. But none of these rules know anything about specific packages. ESLint has no concept that axios.get() throws AxiosError, that prisma.user.create() throws PrismaClientKnownRequestError, or that stripe.charges.create() throws StripeCardError.


Does TypeScript Strict Mode Catch Missing Error Handling?

TypeScript's strict flag enables several compiler checks (strictNullChecks, noImplicitAny, strictFunctionTypes, etc.), but none of them address runtime error handling.

TypeScript knows the return type of axios.get() is Promise<AxiosResponse<T>>. It does not know that this function can throw. TypeScript has no throws clause like Java or Swift. There is no way to declare in a type signature that a function throws a specific error.

import axios from 'axios';

async function getUsers() {
  // TypeScript: "this returns Promise<AxiosResponse<User[]>>"
  // TypeScript does NOT know: "this can throw AxiosError"
  // TypeScript does NOT warn: "you should wrap this in try-catch"
  const response = await axios.get<User[]>('/api/users');
  return response.data;
}

TypeScript strict mode is essential for type safety. It is not designed for behavioral safety. It tells you "this variable might be null." It does not tell you "this function call can fail at runtime and you are not handling it."


Can Semgrep Detect Missing Try-Catch Around API Calls?

Semgrep is a pattern-matching static analysis tool that can be configured to find missing error handling. You write rules that match code patterns.

# .semgrep/rules/axios-try-catch.yaml
rules:
  - id: axios-missing-try-catch
    patterns:
      - pattern: await axios.$METHOD(...)
      - pattern-not-inside: |
          try { ... } catch (...) { ... }
    message: "axios call without try-catch"
    languages: [typescript]
    severity: WARNING

This works for simple cases. The limitations show up fast:

  1. You need to write a separate rule for every package. There is no pre-built library of error handling rules for npm packages.
  2. Pattern matching misses indirect calls. If you assign axios.create() to a variable and call methods on that instance, the pattern await axios.$METHOD(...) will not match.
  3. Semgrep does not understand TypeScript types. It cannot follow that const client: AxiosInstance = axios.create() means client.get() should also be wrapped.
  4. Maintaining rules for 20+ packages across your team is a real cost.

Semgrep is good for one-off checks. For comprehensive package error handling coverage across a codebase, you need something purpose-built.


How Does Nark Check for Missing Error Handling?

Nark takes a different approach. Instead of generic lint rules, it uses Nark profiles: YAML definitions that describe how each package should be used, including what errors it throws and what your code must handle.

The nark-corpus contains profiles for 160+ npm packages. Each profile defines postconditions like "when you call axios.get(), you MUST handle AxiosError" or "when you call prisma.user.create(), you MUST handle PrismaClientKnownRequestError."

npx nark --tsconfig ./tsconfig.json

Nark parses your TypeScript AST, identifies calls to profiled packages, and checks whether the surrounding code meets the profile postconditions. Here is what a Nark scan reports:

src/api/users.ts:14:3
  axios.get() called without error handling
  Profile: axios/postcondition/handle-axios-error
  Severity: ERROR
  Fix: Wrap in try-catch and handle AxiosError

src/db/queries.ts:28:5
  prisma.user.create() called without error handling
  Profile: @prisma/client/postcondition/handle-known-request-error
  Severity: ERROR
  Fix: Wrap in try-catch and handle PrismaClientKnownRequestError

What Nark catches that other tools miss:

  • Instance method calls: const client = axios.create(); client.get('/users') is tracked
  • Chained calls: prisma.user.create(), stripe.charges.create()
  • Package-specific error types: knows that axios throws AxiosError, not just generic Error
  • Missing .on('error') event handlers for packages like redis and pg that use event emitters

ESLint vs TypeScript Strict vs Semgrep vs Nark: Which Should You Use?

Each tool covers a different layer. They are not interchangeable.

CapabilityESLintTypeScript strictSemgrepNark
Floating promisesYes (no-floating-promises)NoYes (custom rule)No (not its focus)
Missing try-catch around awaitNoNoYes (custom rule per package)Yes (160+ packages built-in)
Package-specific error typesNoNoPartially (string matching)Yes (Nark profiles)
Instance method trackingN/AN/ANoYes (follows axios.create() instances)
Chained calls (prisma.user.create())N/AN/APartiallyYes (property chain analysis)
Event handler checks (.on('error'))NoNoCustom rule possibleYes (redis, pg, etc.)
Type-aware analysisLimitedYesNoYes (uses TypeScript AST)
Pre-built rules for npm packagesFewNoneCommunity rules (not error handling)160+ package profiles
Zero configPlugin install + configtsconfig.jsonRule files requirednpx nark
CI integrationYesYesYesYes (SARIF output)

The practical answer: use all of them together.

  • TypeScript strict mode catches null/undefined issues and type mismatches. Turn it on.
  • ESLint no-floating-promises catches forgotten await and unhandled promises. Turn it on.
  • Nark catches missing error handling around specific package calls. Run it in CI.
  • Semgrep is useful for custom project-specific rules that none of the above cover.

How to Enforce Try-Catch Around API Calls in TypeScript?

There are three levels of enforcement, from weakest to strongest.

Level 1: Code review. Your team agrees on error handling conventions and reviews PRs for compliance. This catches maybe 60% of issues. Reviewers miss things, especially in large diffs.

Level 2: Linting. Add @typescript-eslint/no-floating-promises to your ESLint config. This catches unhandled promises but not missing try-catch. It is better than nothing.

Level 3: Profile scanning. Run Nark in your CI pipeline:

# .github/workflows/nark.yml
name: Error Handling Check
on: [pull_request]

jobs:
  nark-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npx nark --tsconfig ./tsconfig.json --format sarif > nark.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: nark.sarif

This uploads SARIF results to GitHub's code scanning tab, so violations appear as annotations directly on the pull request diff. Developers see the missing error handling before the code is merged.


What Do These Tools Actually Miss? A Side-by-Side Example

Consider this typical service file in a Node.js backend:

import axios from 'axios';
import { PrismaClient } from '@prisma/client';
import Stripe from 'stripe';

const prisma = new PrismaClient();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

async function createPaidUser(email: string, paymentMethodId: string) {
  // Step 1: Create Stripe customer
  const customer = await stripe.customers.create({
    email,
    payment_method: paymentMethodId,
  });

  // Step 2: Save to database
  const user = await prisma.user.create({
    data: { email, stripeCustomerId: customer.id },
  });

  // Step 3: Notify external service
  await axios.post('https://hooks.example.com/new-user', {
    userId: user.id,
  });

  return user;
}

What each tool reports:

ToolFindings
TypeScript strict0 errors (all types are correct)
ESLint no-floating-promises0 warnings (all promises are awaited)
Semgrep (no custom rules)0 findings (no rules configured)
Nark3 violations: stripe.customers.create() missing error handling, prisma.user.create() missing error handling, axios.post() missing error handling

All three calls in that function can throw at runtime. Stripe throws StripeCardError if the payment method is invalid. Prisma throws PrismaClientKnownRequestError with code P2002 if the email already exists. Axios throws AxiosError if the webhook endpoint is down. None of those failures are handled. Only Nark flags all three.


How to Check If Your Codebase Handles All Error Cases?

Run a full scan with Nark to measure your error handling coverage across the entire codebase:

npx nark --tsconfig ./tsconfig.json

Nark reports every call to a profiled package that lacks proper error handling. The output includes the file, line number, which profile was violated, and what the fix should be. Here is what typical output looks like on a project with gaps:

nark v1.x — scanning against 160+ profiles

src/api/users.ts:14:3
  axios.get() called without error handling
  Profile: axios/postcondition/handle-axios-error
  Severity: ERROR

src/db/queries.ts:28:5
  prisma.user.create() called without error handling
  Profile: @prisma/client/postcondition/handle-known-request-error
  Severity: ERROR

src/payments/charge.ts:9:3
  stripe.charges.create() called without error handling
  Profile: stripe/postcondition/handle-stripe-error
  Severity: ERROR

Scanned 42 files | 3 violations found | 3 packages affected

How to interpret the results: Each violation means a specific package call can throw a known error type at runtime and your code does not handle it. Start with Severity: ERROR violations — these are the calls most likely to cause unhandled promise rejections or crashes in production. The profile name tells you exactly which error type to catch (e.g., AxiosError, PrismaClientKnownRequestError).

Measuring error handling coverage over time: Run Nark in CI on every pull request. Track the violation count across releases. A codebase with zero Nark violations has full error handling coverage for all profiled packages — meaning every known throwing call site is wrapped in appropriate error handling.

For a comprehensive check, combine tools at every layer:

  1. Enable @typescript-eslint/no-floating-promises in ESLint to catch unawaited promises
  2. Run npx nark to catch missing try-catch around package calls
  3. Use process.on('unhandledRejection', ...) in your entry point as a runtime safety net (not a fix — a last resort)

The goal is to catch missing error handling before it reaches production. ESLint and TypeScript catch the generic cases. Nark catches the package-specific cases that the other tools are not designed to detect.


What Are the Best TypeScript Linter Rules for Error Handling?

These are the five most important TypeScript ESLint rules for catching error handling problems. Enable all of them.

  1. @typescript-eslint/no-floating-promises — Flags promises that are not awaited, .catch()'d, or .then()'d. This is the single most impactful rule for preventing unhandled promise rejections.

  2. @typescript-eslint/no-misused-promises — Catches promises passed to places that don't expect them, like if (fetchUser()) where the condition is always truthy because it's a Promise object.

  3. no-throw-literal — Ensures you only throw Error objects (or subclasses), not strings or plain objects. This makes catch blocks predictable.

  4. no-unsafe-finally — Prevents control flow statements (return, throw, break, continue) inside finally blocks, which silently swallow errors.

  5. require-await — Flags async functions that don't contain an await. These are often mistakes where the developer forgot to await an async call, leading to silently dropped errors.

These rules cover general error handling hygiene. For package-specific checks — like "did you wrap prisma.user.create() in a try-catch?" — you need Nark, which goes beyond what lint rules can express.


Frequently Asked Questions

Is there a linter rule for unhandled async errors?

Yes. @typescript-eslint/no-floating-promises catches promises that are not awaited or .catch()'d. It does not check whether awaited promises are inside try-catch blocks. For that, you need Nark or custom Semgrep rules.

Does TypeScript have a throws keyword like Java?

No. TypeScript has no way to declare that a function throws a specific error type. This is a known limitation. Proposals exist (see TypeScript issue #13219) but none have been accepted. This is exactly the gap that Nark profiles fill: they declare throwing behavior outside the type system.

Can I use Nark with JavaScript (not TypeScript)?

Nark scans the TypeScript AST, so it needs a tsconfig.json. If your project is JavaScript, you can add a tsconfig.json with allowJs: true and checkJs: true to scan .js files.

Does Nark replace ESLint?

No. Nark checks package-specific Nark profiles. ESLint checks general code quality rules. Use both. They cover different things.

How many packages does Nark have profiles for?

The nark-corpus currently contains profiles for 160+ npm packages, including axios, prisma, stripe, pg, redis, ioredis, openai, aws-sdk, nodemailer, and many more. The list grows with each release.

Can I write my own Nark profiles?

Yes. Profiles are YAML files that follow the nark-corpus schema. You can add profiles for internal packages or packages not yet in the corpus.


Try It Now

npx nark --tsconfig ./tsconfig.json

Nark checks 160+ packages — including axios, prisma, stripe, and redis — for correct error handling, resource cleanup, and profile compliance. Zero config. Takes about 30 seconds on a typical project.