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
returnstatements - Unreachable code
- TypeScript type violations (via
@typescript-eslint) - Code style issues (formatting, naming conventions)
- Potential
nulldereferences 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.responsebefore accessing it on anAxiosError - Whether you handled
PrismaClientKnownRequestErrorP2002 on a unique constraint - Whether your Stripe charge handles
StripeCardErrorseparately fromStripeRateLimitError
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 (throwsAxiosErroron any non-2xx response) prisma.user.create()without catchingPrismaClientKnownRequestError(throws P2002 on duplicate email)stripe.charges.create()without catchingStripeCardError(unhandled card declines)redis.get()without an.on('error')handler (process crash on connection failure)openai.chat.completions.create()without handlingRateLimitError(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
| Capability | ESLint | Semgrep | Nark |
|---|---|---|---|
| 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 externally | Plugins | Semgrep registry | nark-corpus |
| IDE integration | ✅ VS Code, JetBrains | ✅ VS Code | Planned |
| 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.