← Back to Blog

Why AI-Generated TypeScript Code Skips Error Handling (And How to Catch It)

By Nark Team

AI coding assistants — Copilot, Cursor, Claude, Gemini — generate TypeScript that compiles, passes ESLint, and looks correct on inspection. The problem is what they don't know: the specific runtime error behaviors of the npm packages they're calling. AI-generated API integrations routinely omit the error handling that prevents production crashes.

Quick Answer: AI assistants learn from TypeScript type definitions and documentation, which describe a package's API surface but not its complete runtime error behavior. The result is syntactically correct code that crashes in production. Nark detects these gaps automatically: npx nark --tsconfig ./tsconfig.json


What AI Assistants Know About npm Packages

When an AI assistant generates code that calls axios.get(), it knows:

  • The function signature ((url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>)
  • Common usage patterns from the training data
  • The TypeScript types for the response object

What it frequently doesn't encode correctly:

  • axios.get() throws AxiosError for ALL non-2xx responses (not just 5xx)
  • AxiosError has three distinct shapes depending on where the failure occurred
  • Accessing error.response.status without checking error.response existence crashes on network failures
  • The error.code values (ECONNABORTED, ETIMEDOUT, ERR_NETWORK) and what each means

AI assistants know the type system. They often don't know the runtime invariants.


A Real Example: Copilot Writes an axios Request

Ask Copilot or Cursor to "add an API call to fetch users from /api/users." A typical output:

// AI-generated — looks reasonable, compiles, passes ESLint
async function fetchUsers(): Promise<User[]> {
  try {
    const response = await axios.get<User[]>('/api/users');
    return response.data;
  } catch (error) {
    console.error('Failed to fetch users:', error);
    throw error;
  }
}

This code:

  • Compiles: ✅
  • Passes @typescript-eslint/no-floating-promises: ✅
  • Handles all axios error cases: ❌

The catch block logs and re-throws. In production:

  • A 404 response logs AxiosError with status 404 — probably fine
  • A network timeout logs AxiosError with error.code = 'ECONNABORTED' — no timeout-specific handling
  • A CORS error in the browser logs AxiosError with error.code = 'ERR_NETWORK' — same handling as a timeout
  • Any code that subsequently reads error.response?.status will get undefined on network errors

The function re-throws all errors identically. The caller receives an AxiosError and has no information about whether it's retryable, user-facing, or a bug.

What correct handling looks like:

async function fetchUsers(): Promise<User[]> {
  try {
    const response = await axios.get<User[]>('/api/users');
    return response.data;
  } catch (error: unknown) {
    if (!axios.isAxiosError(error)) throw error;

    if (error.response) {
      // HTTP error — server responded with 4xx/5xx
      if (error.response.status === 404) return [];
      throw new ApiError(error.response.status, error.response.data);
    }

    if (error.request) {
      // Network error — request sent, no response
      throw new NetworkError(error.code ?? 'UNKNOWN');
    }

    // Setup error — request not sent
    throw new ConfigError(error.message);
  }
}

Nark reports the AI-generated version as a violation. The corrected version passes.


A Real Example: AI Writes a Prisma User Creation

Ask AI to "create a user in the database with email and password." A typical output:

// AI-generated — compiles, runs, crashes on duplicate email
async function createUser(email: string, hashedPassword: string): Promise<User> {
  const user = await prisma.user.create({
    data: {
      email,
      password: hashedPassword,
    },
  });
  return user;
}

No error handling at all. In a registration endpoint, a duplicate email throws PrismaClientKnownRequestError with code P2002. The error propagates up, hits your generic error handler (if you have one), and returns a 500 to the user instead of "Email already taken."

A more complete AI prompt ("create a user, handle duplicate email") produces better output:

async function createUser(email: string, hashedPassword: string): Promise<User> {
  try {
    return await prisma.user.create({
      data: { email, password: hashedPassword },
    });
  } catch (error) {
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      if (error.code === 'P2002') {
        throw new ConflictError('Email already in use');
      }
    }
    throw error;
  }
}

But this requires you to know to ask. Nark detects the first version as a violation so you don't have to remember.


Why AI Assistants Produce This Pattern

The root cause is how AI assistants were trained and what they optimize for.

Training data skews toward happy path code. Open-source repositories contain far more successful API calls than proper error handling. Tutorial code, Stack Overflow answers, and documentation examples show how to call axios.get() — not how to handle every failure mode. The model learns the common pattern, which is the happy path.

TypeScript types don't encode exceptions. TypeScript's type system doesn't track what a function throws. When an AI assistant reads axios.get(): Promise<AxiosResponse<T>>, it sees a function that returns a response. The fact that it also throws AxiosError in numerous specific situations is not in the type signature. It's in documentation, changelogs, and GitHub issues — not always in training data.

Prompts rarely specify error handling. "Write a function to call this API" produces happy-path code. The implicit contract between developer and AI is to produce working code, and working code in the green path often compiles and passes basic checks. Error handling requires explicit prompting.

AI models optimize for tests passing, not production robustness. The feedback loops that reinforce AI behavior (does it compile? does the test pass?) don't include "does the error handling correctly distinguish network failures from card declines?"


The Scope of the Problem

This is not a theoretical concern. Nark scanned a corpus of TypeScript repositories and found:

  • Unhandled axios calls in the majority of projects that import axios
  • Prisma calls without P2002 handling in nearly every project with a user registration flow
  • Redis clients without .on('error') handlers in most Redis-using projects

When teams adopt AI coding assistants at scale — where 30-50% of new code is AI-generated — these gaps compound. Each AI-generated PR adds more unhandled calls. Code review catches some, but not all. The ones that make it through are the ones that crash production.


Adding Nark as a Quality Gate for AI-Generated Code

The practical solution is to add Nark to your CI pipeline. Every PR — whether human-written or AI-generated — gets checked for dependency error handling coverage.

# .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: TypeScript type check
        run: npx tsc --noEmit

      - name: ESLint
        run: npx eslint src --ext .ts,.tsx

      - name: nark — dependency error handling (catches AI blind spots)
        run: npx nark --tsconfig tsconfig.json

With Nark in CI:

  • AI-generated axios call without proper error handling → PR blocked ✅
  • AI-generated Prisma mutation without P2002 handling → PR blocked ✅
  • AI-generated Redis client without .on('error') → PR blocked ✅

The developer can then ask their AI assistant to fix the specific violation Nark reported, which tends to produce much better output because the prompt is specific: "handle StripeCardError separately from StripeRateLimitError in this catch block."


Using Nark to Improve AI Prompts

Nark's violation messages describe the specific postcondition that's missing. You can use these as context for the next AI prompt:

Step 1: Run Nark, get a violation:

src/payments/charge.ts:52 — stripe
  [ERROR] stripe.charges.create() — StripeCardError not handled
  Stripe throws StripeCardError when a card is declined. This requires
  user-facing handling distinct from StripeRateLimitError (retry) and
  network errors (retry with backoff).

Step 2: Paste the violation into your AI assistant:

"Fix this Nark violation: stripe.charges.create() on line 52 doesn't handle StripeCardError separately. StripeCardError is a declined card and needs a user-facing message. StripeRateLimitError should retry with backoff."

Step 3: AI generates the correct fix, guided by the specific postcondition.

This workflow — AI writes code, Nark catches the gaps, AI fixes the specific violation — produces significantly better error handling than asking AI to write complete error handling from scratch.


Frequently Asked Questions

Does this mean AI-generated code is unsafe to use?

No. AI coding assistants are valuable tools. The issue is a specific category of bug — npm package error handling — that falls outside what AI learns well from type definitions and training data. The solution is a quality gate (Nark in CI), not avoiding AI-generated code.

Will AI assistants get better at this over time?

Possibly, but the feedback loops are slow. Nark provides immediate, deterministic feedback on every PR. Whether AI models eventually learn all runtime error contracts for all npm packages, the checking mechanism is still valuable.

Can I prompt my AI assistant to always handle errors correctly?

You can include error handling instructions in your system prompt or .cursorrules. This helps but isn't complete — the AI still needs to know what specific errors to handle for each package. A tool that checks the output is more reliable than relying on prompt consistency.

How long does Nark take to run in CI?

About 30 seconds on a typical TypeScript project. It uses the TypeScript compiler for parsing, which adds some overhead, but the scan itself is fast.


Try It Now

npx nark --tsconfig ./tsconfig.json

Nark checks 160+ packages — including axios, prisma, stripe, and redis — for correct error handling and Nark profiles. Add it to your CI pipeline as a quality gate for AI-generated code — the layer that catches what TypeScript types and ESLint rules can't see.