← Back to Blog

The Three Layers of TypeScript Dependency Safety: Supply Chain, SAST, and Completeness

By Nark Team

Most TypeScript teams run one or two layers of dependency checking: a vulnerability scanner on their lockfile and a linter on their source code. Production incidents still happen because there is a third layer nobody checks. You install a safe package, write syntactically correct code that calls it, and ship. Then axios.get() throws AxiosError on a 429 response and your API returns 500 because nobody wrapped the call in a try-catch. The package was safe. The code was valid. The error handling was missing.

Quick Answer: Layer 1 (Socket.dev, Snyk) checks if packages are safe to install. Layer 2 (Semgrep, ESLint) checks if your code is written securely. Layer 3 (Nark) checks if your code handles the runtime errors your packages actually throw. Most teams have Layers 1 and 2. Almost none have Layer 3. Run all three: npx nark --tsconfig ./tsconfig.json


What Are the Three Layers?

Every npm dependency in a TypeScript project passes through three risk surfaces. Each layer has its own tools, its own failure mode, and its own category of production incident.

Layer 1: Is This Package Safe to Install?

The question: Does this package contain malicious code, known vulnerabilities, or supply chain risks?

Tools: Socket.dev, Snyk, npm audit, GitHub Dependabot

What they catch:

  • Malicious install scripts that exfiltrate .env files
  • Typosquatting (axois instead of axios)
  • Known CVEs in pinned versions
  • Compromised maintainer accounts publishing new versions with backdoors
  • Obfuscated code in published tarballs
  • License violations

What they miss:

  • How your code calls the package
  • Whether your error handling matches what the package throws
  • Whether your catch blocks handle the right error types

Example failure this layer prevents: A compromised version of event-stream ships a postinstall script that reads ~/.npmrc and sends auth tokens to an attacker's server. Socket.dev flags the suspicious network access before the package installs.

Layer 2: Is My Code Written Securely?

The question: Does my source code contain security anti-patterns, dangerous function calls, or style violations?

Tools: Semgrep, ESLint (with security plugins), CodeQL, SonarQube

What they catch:

  • SQL injection via string concatenation in queries
  • Hardcoded API keys and secrets
  • Cross-site scripting (XSS) in template rendering
  • Insecure use of eval(), Function(), or unsafe deserialization
  • Floating promises (unawaited async calls)
  • Deprecated API usage

What they miss:

  • Whether your try-catch handles the specific error type a package throws
  • Whether error.response exists before you access it on an AxiosError
  • Whether you catch P2002 separately from P2025 on a Prisma query
  • Whether your Stripe integration distinguishes StripeCardError from StripeRateLimitError

Example failure this layer prevents: A developer writes db.query("SELECT * FROM users WHERE id = " + userId). Semgrep flags the SQL injection. The developer switches to parameterized queries.

Layer 3: Does My Code Handle Runtime Errors Correctly?

The question: For every npm package call in my codebase, did I handle the errors that package can throw at runtime?

Tools: Nark

What it catches:

  • axios.get() without try-catch (throws AxiosError on 4xx, 5xx, network failure, timeout)
  • prisma.user.create() without catching PrismaClientKnownRequestError (throws P2002 on duplicate email)
  • stripe.charges.create() without catching StripeCardError (payment silently fails)
  • redis.get() without .on('error') handler (process crashes on disconnect)
  • @anthropic-ai/sdk calls without handling RateLimitError (AI feature silently breaks)
  • jsonwebtoken.verify() without catching JsonWebTokenError (auth bypass on malformed tokens)

What it misses:

  • Whether the package itself is malicious (Layer 1's domain)
  • Whether your code has SQL injection or XSS (Layer 2's domain)

Example failure this layer prevents: A developer writes await axios.get('/api/users') inside an Express route handler. No try-catch. The upstream API returns 503 during a deployment. axios throws AxiosError. The Express handler crashes. The user sees a 500. Nark flags this before it ships.


Why Each Layer Misses What the Others Catch

The three layers are not redundant. They check fundamentally different things.

You install axios         → Layer 1 says: "Safe package. No CVEs. No malicious code."
You call axios.get()      → Layer 2 says: "Valid syntax. No security issues."
You skip the try-catch    → Layer 3 says: "ERROR: axios.get() called without try-catch"
Your API crashes at 2am   → This is the incident Layer 3 prevents.

Here is the same scenario with Prisma:

You install @prisma/client → Layer 1 says: "Safe package."
You call prisma.user.create() → Layer 2 says: "No SQL injection (Prisma is parameterized)."
You skip catching P2002   → Layer 3 says: "ERROR: prisma.user.create() without P2002 handling"
User signs up twice, gets 500 → This is the incident Layer 3 prevents.

And with Stripe:

You install stripe        → Layer 1 says: "Safe package."
You call stripe.charges.create() → Layer 2 says: "API key not hardcoded. Good."
You skip catching StripeCardError → Layer 3 says: "ERROR: unhandled StripeCardError"
Card declines, payment drops silently → This is the incident Layer 3 prevents.

No single tool covers all three scenarios. Each layer requires knowledge that the other layers do not have.


The Coverage Matrix

RiskLayer 1 (Socket.dev, Snyk)Layer 2 (Semgrep, ESLint)Layer 3 (Nark)
Malicious npm packageCaught--
Known CVE in dependencyCaught--
Typosquatting attackCaught--
SQL injection-Caught-
Hardcoded secrets-Caught-
XSS in templates-Caught-
Floating promise (unawaited)-Caught (ESLint)-
Missing try-catch on axios--Caught
Wrong error type in catch block--Caught
Prisma P2002 not handled--Caught
Stripe card decline unhandled--Caught
Redis client missing error handler--Caught
AI SDK rate limit not handled--Caught

How to Run All Three Layers in CI

# .github/workflows/three-layer-safety.yml
name: Three-Layer Dependency Safety

on: [pull_request]

jobs:
  # Layer 1: Supply chain security
  supply-chain:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Socket.dev — detect malicious/vulnerable packages
        uses: SocketDev/socket-security-action@v1

  # Layer 2: Static application security testing
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Semgrep — security patterns and anti-patterns
        uses: returntocorp/semgrep-action@v1
        with:
          config: p/typescript

  # Layer 3: Runtime error handling completeness
  completeness:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - name: nark — missing error handling for npm packages
        run: npx nark --tsconfig tsconfig.json

All three jobs run in parallel. Each checks a different dimension. A failure in any one blocks the PR.


What Happens Without Each Layer

Without Layer 1: Supply chain attacks go undetected

Your CI installs a compromised package. A postinstall script runs arbitrary code on your build server. Secrets leak. You find out from a security advisory weeks later.

Without Layer 2: Security bugs ship to production

A developer concatenates user input into a database query. A customer discovers the SQL injection before your security team does. The breach makes the news.

Without Layer 3: Legitimate packages crash your app

Your code calls axios.get() without a try-catch. The upstream API returns 503 during peak traffic. Every request to your API endpoint returns 500 for the duration of the outage. Your monitoring alerts fire at 2am. The fix is a three-line try-catch that should have been there from the start.

Layer 3 failures are the most common production incidents in TypeScript applications. They are also the easiest to prevent. The code is correct in every way except one: it does not handle the error case that the package documentation says will happen.


Why Layer 3 Is the Missing Layer

Layer 1 is solved. Dependabot is on by default in GitHub. Most teams also run Snyk or Socket.dev.

Layer 2 is common. ESLint ships with every TypeScript starter template. Semgrep adoption is growing.

Layer 3 is almost nonexistent. Before Nark, the only way to check this was manual code review: read every npm package's documentation, learn what it throws, and verify that every call site handles those errors. For a project with 10 dependencies, that is manageable. For a project with 200 dependencies, it is not.

Nark automates this with a corpus of 160+ Nark Profiles — covering axios, prisma, stripe, redis, and more. Each profile specifies what a package throws, when it throws it, and what correct handling looks like. The scanner reads your TypeScript AST and reports every call site that does not match the profile.

npx nark --tsconfig ./tsconfig.json
ERROR  axios         axios.get() called without try-catch
       src/lib/api.ts:23  in fetchUsers()

ERROR  @prisma/client  prisma.user.create() without P2002 handling
       src/routes/signup.ts:47  in createUser()

ERROR  stripe        stripe.charges.create() without StripeCardError handling
       src/billing/charge.ts:12  in chargeCustomer()

3 violations found across 3 packages

Each violation is a potential production incident that Layer 1 and Layer 2 cannot see.


Frequently Asked Questions

Do I really need all three layers?

If you ship TypeScript to production: yes. Each layer catches a category of bug that the other two cannot detect. Running only Layer 1 and Layer 2 is like having a fire alarm and a burglar alarm but no smoke detector in the kitchen.

What if I already use Dependabot and ESLint?

Dependabot covers Layer 1 (CVE updates). ESLint covers part of Layer 2 (code style, floating promises). You are missing Semgrep-style security scanning (Layer 2 depth) and runtime error handling checks (Layer 3 entirely). Adding Nark takes 30 seconds: npx nark --tsconfig ./tsconfig.json.

Is Layer 3 the same as writing more unit tests?

No. Unit tests verify behavior you thought to test. Layer 3 checks whether you handled error cases you may not have thought about at all. Nark knows that prisma.user.create() throws P2002 on duplicate records because that information comes from the Prisma documentation and changelog. Your unit test suite only covers the cases someone wrote a test for.

Does Nark replace Socket.dev or Semgrep?

No. Nark does not inspect packages for malware (Socket's job) or check for SQL injection (Semgrep's job). Nark checks one thing: did your code handle the runtime errors your dependencies throw? The three tools are complementary.

How does Nark know what packages throw?

Each Nark Profile is a YAML file built from the package's official documentation, changelog, GitHub issues, and real production incident reports. The profiles specify which functions throw, what error types they produce, and what handling is required. The full corpus is open source at nark-corpus.

Which packages does Nark cover?

Nark ships with profiles for 160+ npm packages including axios, Prisma, Stripe, Redis, pg, OpenAI, Anthropic, Twilio, AWS SDK, jsonwebtoken, bcrypt, nodemailer, and more. The list grows with community contributions.


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 runtime Nark Profiles. Add it as the third layer in your CI pipeline alongside Socket.dev and Semgrep to cover all three dimensions of TypeScript dependency safety.