← Back to Blog

ESLint vs Semgrep vs Nark: What Each Catches in TypeScript

By Nark Team

ESLint, Semgrep, and Nark are three distinct TypeScript static analysis tools that catch different categories of bugs. ESLint enforces code style and language-level rules. Semgrep detects security vulnerabilities and custom anti-patterns. Nark checks whether your code correctly handles the error cases that your npm dependencies can throw at runtime. Most production-quality TypeScript projects need all three.

Quick Answer: ESLint catches code style and JS/TS language bugs. Semgrep catches security vulnerabilities and team-specific anti-patterns. Nark catches unhandled runtime errors from npm packages (axios, Prisma, Stripe, etc.). They do not overlap. To run Nark on your project: npx nark --tsconfig ./tsconfig.json


What Is ESLint and What Does It Catch?

ESLint is a static analysis linter for JavaScript and TypeScript. It parses your code into an AST (Abstract Syntax Tree) and applies rules at the language level.

What ESLint catches:

  • Unused variables and imports
  • Missing return statements
  • Unreachable code
  • TypeScript type violations (via @typescript-eslint)
  • Code style issues (formatting, naming conventions)
  • Potential null dereferences when combined with strict TypeScript rules
  • no-floating-promises — unawaited async calls

What ESLint does not catch:

  • Whether your try-catch actually handles the specific error a library throws
  • Whether you checked error.response before accessing it on an AxiosError
  • Whether you handled PrismaClientKnownRequestError P2002 on a unique constraint
  • Whether your Stripe charge handles StripeCardError separately from StripeRateLimitError

ESLint's no-floating-promises rule catches bare await calls without any error handling at all — but it does not know what those promises throw. It treats all async functions the same, regardless of which npm package they come from.

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

// eslint no-floating-promises: does NOT catch this (try-catch present)
// but nark DOES catch this (catch doesn't handle StripeCardError)
try {
  await stripe.charges.create({ amount: 1000, currency: 'usd', source: token });
} catch (error) {
  console.error(error); // logs but doesn't distinguish card declines from rate limits
}

What Is Semgrep and What Does It Catch?

Semgrep is a semantic code search and analysis tool. You write patterns in a YAML syntax that match code structures, and Semgrep finds all instances of that pattern across your codebase.

What Semgrep catches:

  • Security vulnerabilities: SQL injection, hardcoded secrets, insecure crypto
  • Dangerous function calls: eval(), unsafe deserialization
  • Deprecated API usage
  • Custom anti-patterns your team defines
  • Cross-language patterns (same rule works in Python, Java, Go, TypeScript)

What Semgrep does not catch (by default):

  • Package-specific error handling gaps — Semgrep has no built-in rules for "are you handling axios network errors correctly?"
  • Whether your Prisma code catches connection errors vs query errors
  • Runtime behavior of npm packages in general

Semgrep's strength is security rules and custom organizational patterns. The public Semgrep registry has thousands of security-focused rules. But it does not ship with rules about npm package error handling behavior — that knowledge has to come from somewhere else.

# Semgrep rule example — catches hardcoded secrets
rules:
  - id: no-hardcoded-api-key
    pattern: |
      const $KEY = "sk_live_..."
    message: Hardcoded Stripe API key detected
    severity: ERROR

You would need to write and maintain similar rules for every npm package's error handling behavior — and keep them up-to-date as packages evolve. This is exactly the problem Nark solves.


What Is Nark and What Does It Catch?

Nark is a Nark Profile scanner for TypeScript. It uses the TypeScript compiler (tsc) to parse your code and then checks it against a library of profiles — machine-readable YAML specifications of what npm packages can throw at runtime, what error types they use, and what handling is required.

What Nark catches:

  • Bare axios.get() calls without try-catch (throws AxiosError on any non-2xx response)
  • prisma.user.create() without catching PrismaClientKnownRequestError (throws P2002 on duplicate email)
  • stripe.charges.create() without catching StripeCardError (unhandled card declines)
  • redis.get() without an .on('error') handler (process crash on connection failure)
  • openai.chat.completions.create() without handling RateLimitError (silent failures in AI features)
  • Instance method calls — detects calls on axios.create() instances, not just the default import

What Nark does not catch:

  • Code style issues (ESLint's domain)
  • Security vulnerabilities like SQL injection (Semgrep's domain)
  • General TypeScript type errors (the TypeScript compiler's domain)

Nark's rules come from reading npm package changelogs, GitHub issues, official documentation, and real-world production incident reports. Each profile is a binary check: either your code handles the error case or it doesn't.

// nark catches: no error handling on axios
const response = await axios.get('https://api.example.com/users');

// nark catches: generic catch without checking axios.isAxiosError()
try {
  const response = await axios.get('https://api.example.com/users');
} catch (e) {
  throw e; // re-throw without inspecting — network errors lose context
}

// nark passes: proper error handling
try {
  const response = await axios.get<User[]>('https://api.example.com/users');
  return response.data;
} catch (error) {
  if (axios.isAxiosError(error)) {
    if (error.response) {
      throw new HttpError(error.response.status, error.response.data);
    }
    throw new NetworkError(error.code ?? 'UNKNOWN', error.message);
  }
  throw error;
}

Side-by-Side Comparison

CapabilityESLintSemgrepNark
Code style enforcement✅ Core strength
TypeScript type checking✅ via @typescript-eslint
Security vulnerabilities⚠️ Limited✅ Core strength
npm package error handling❌ (manual rules only)✅ Core strength
Custom rule authoring✅ Plugins✅ YAML patterns✅ YAML profiles
Knows what axios throws
Knows what Prisma throws
Knows what Stripe throws
Rules maintained externallyPluginsSemgrep registrynark-corpus
IDE integration✅ VS Code, JetBrains✅ VS CodePlanned
CI integration
Free tier✅ (open source)

Why ESLint Alone Is Not Enough

ESLint's @typescript-eslint/no-floating-promises rule is the closest ESLint gets to catching missing error handling. It flags async calls that are not awaited or not wrapped in a promise chain. But it treats all async functions identically — it has no knowledge that axios.get() and fetch() have different error behaviors.

// @typescript-eslint/no-floating-promises: PASSES (try-catch present)
// nark: FAILS (AxiosError network error case not handled)
async function loadUsers() {
  try {
    const response = await axios.get('/users');
    return response.data;
  } catch (error) {
    // error.response may be undefined on network failure
    // this crashes when error.response is undefined
    console.error(error.response.data); // TypeError: Cannot read properties of undefined
  }
}

ESLint sees a try-catch and moves on. Nark sees that the catch block accesses error.response without first checking whether it exists — a bug that causes a TypeError crash on network failures, not HTTP errors.


Why Semgrep Alone Is Not Enough

Semgrep is excellent at security rules. For npm package error handling, you would need to author, test, and maintain a rule for every package your team uses. This means:

  • Reading each package's documentation to understand what it throws
  • Writing a Semgrep pattern that matches every call pattern (direct imports, re-exports, class instances, destructured methods)
  • Updating rules when packages release new error types or deprecate old ones
  • Getting TypeScript's type information (Semgrep pattern matching is structural, not type-aware for all cases)

Nark's profiles already encode this work for 160+ npm packages — including axios, prisma, stripe, and redis. The corpus is open-source and maintained by the community.


The Production-Ready Stack

Run all three in CI:

# .github/workflows/quality.yml
name: Code Quality

on: [pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: ESLint — style and language rules
        run: npx eslint src --ext .ts,.tsx

      - name: Semgrep — security patterns
        uses: returntocorp/semgrep-action@v1
        with:
          config: p/typescript

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

Each tool runs independently. A failure in any one fails the CI check. Together, they cover style, security, and package error handling — three dimensions that have zero overlap.


Frequently Asked Questions

Does ESLint no-unsafe-* cover npm package errors?

No. The @typescript-eslint/no-unsafe-* rules (no-unsafe-call, no-unsafe-member-access, etc.) flag type-unsafe operations, not missing error handling. A try-catch that catches the wrong error shape passes all unsafe rules but still crashes in production.

Can Semgrep write rules that check axios error handling?

Yes, but with limitations. You can write a Semgrep pattern that finds axios.get(...) calls not inside a try-catch. You cannot easily write a rule that checks the shape of the catch block — whether it uses axios.isAxiosError(), whether it checks error.response existence, whether it handles network errors differently from HTTP errors. Nark profiles encode these distinctions precisely.

Do Nark and ESLint conflict?

No. They check different things. ESLint's no-floating-promises and Nark's profile checks complement each other: ESLint catches completely unhandled async calls, Nark catches incorrectly handled async calls.

Is Nark open source?

Yes. Nark is AGPL-3.0. The profile corpus (nark-corpus) is CC-BY-NC-4.0 — anyone can read and verify every rule.


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 Nark profiles. Add it to your CI pipeline alongside ESLint and Semgrep to cover all three dimensions of TypeScript code quality.