Skip to content
·10 min read

Error Handling Patterns AI Forgets in Vibe Coded Apps

The five categories of errors AI tools quietly skip, and the patterns that turn silent failures into visible, recoverable ones

Share

Error handling patterns are the difference between an app that fails loudly and one that fails silently. AI coding tools have a strong bias toward the happy path, the sequence of events that occurs when nothing goes wrong, because that is what most prompts describe. The result is code that compiles, passes a manual test, and then loses data the first time a third-party API has a bad day.

This piece is a tour of the five error handling patterns AI consistently forgets, why each one matters in production, and what to add to make the AI's output actually robust.

Why AI Skips Error Paths

Large language models are trained on code, and most of the code they have seen optimizes for readability, not resilience. A tutorial showing how to call a payment API does not include the 50 lines of timeout, retry, idempotency, and reconciliation logic that production code needs, because that would obscure the lesson. The AI absorbs the tutorial pattern and reproduces it when you ask for similar code.

There is also a prompt asymmetry. When you ask an AI to "build a checkout flow," the implicit request is for the success path. Failure paths are infinite, every external call has dozens of failure modes, and the AI has no way to know which ones matter to your app. So it skips them. This is rational behavior given the prompt, and pathological behavior given production.

Key Takeaway

A 2025 analysis of AI-generated production code found that 78% of network calls had no explicit timeout, 65% had no retry logic, and 41% silently swallowed exceptions in a try/catch that returned default values. None of those omissions show up in a working demo.

The fix is not to abandon AI for these tasks. The fix is to know the patterns AI forgets and ask for them by name. Each section below covers one pattern, the failure mode it prevents, and the specific code shape to look for.

Pattern 1: Timeouts on Every External Call

The default behavior of fetch in Node.js, until very recently, was to wait forever. The default behavior of most HTTP clients is similar. If a third-party API hangs without responding, your code will hang too, holding the connection open until something kills the process. On a serverless function, that means the function times out after 30 seconds. On a long-running server, it means a single slow API can hang every request.

The pattern is simple, every external call needs a maximum wait time. Five seconds is a reasonable default for user-facing operations, ten seconds for background jobs.

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch(url, { signal: controller.signal });
  return await response.json();
} finally {
  clearTimeout(timeout);
}

When AI generates an external API call, scan it for fetch( or your library's equivalent, and ask, what happens if this never returns. If the answer is "the user waits forever," add a timeout.

EXPLAINER DIAGRAM titled THE ANATOMY OF A TIMEOUT showing a horizontal timeline on a slate background with three rows for three scenarios. Top row labeled NO TIMEOUT shows a request box on the left, a long thin line stretching across the entire timeline with a label reading WAITING FOREVER, ending in a red X labeled FUNCTION KILLED AT 30 SECONDS. Middle row labeled TIMEOUT NO RETRY shows the same request, a shorter line ending at a red X at the 5 second mark labeled TIMEOUT FIRED, then a small box labeled USER SEES ERROR. Bottom row labeled TIMEOUT WITH RETRY shows the request, a 5 second line ending in a yellow circle labeled RETRY, then another 5 second line ending in a green check labeled SUCCESS, ending in a box labeled USER SEES OK. A vertical scale on the left labeled TIME runs from 0 SECONDS at the top to 30 SECONDS at the bottom. A footer reads SET TIMEOUTS BEFORE YOU NEED THEM.
The same failed third-party call produces three completely different outcomes based on whether timeout and retry logic exist.

The single biggest cause of preventable production incidents in vibe coded apps is missing timeouts on outbound HTTP calls. The fix takes two lines and prevents a category of outage. AI never adds it unless you ask.

Pattern 2: Retries with Backoff

The second pattern AI forgets is retries. A network call that fails because of a transient blip, packet loss, brief DNS hiccup, momentarily overloaded API, will succeed on retry roughly 90% of the time. Without retries, that 90% becomes user-visible errors that did not need to happen.

The pattern is exponential backoff, retry the failed call after 1 second, then 2 seconds, then 4 seconds, with a small random jitter to avoid stampeding herds when many clients retry at once. Cap the total at 3 to 5 attempts, after that, the call is genuinely broken and should propagate the error.

The catch is that retries only make sense for idempotent operations, calls that can be safely repeated. A GET is always idempotent. A POST that creates a charge is dangerously not, retrying a payment can charge the customer twice. This leads to the next pattern.

Pattern 3: Idempotency Keys

If you retry a payment, you might charge the customer twice. If you retry a webhook delivery, you might send the same email twice. The fix is an idempotency key, a unique identifier the client generates and the server uses to detect and reject duplicates.

Stripe documents this pattern well. Every charge request includes an Idempotency-Key header with a unique string. Stripe stores the result of the first request and returns the same result if the same key arrives again, regardless of whether the second request actually arrived during a retry or was a deliberate duplicate.

Build apps that fail well

Read the rest of the reliability series

Browse the ship category

For your own server, the implementation is, generate a UUID per logical operation, store the operation result keyed by that UUID, and check the store before processing. If the key already exists, return the stored result. If it does not, process the operation and store the result. AI can write this pattern correctly when prompted, but never adds it spontaneously, because most demo code assumes operations happen exactly once.

Pattern 4: Failed Operation Queues

Some operations cannot fail silently and cannot be retried inline. A failed email, a failed webhook, a failed payment notification, all need to be retried later, possibly hours or days later, after a transient outage clears. The pattern for this is a dead letter queue, a database table or queue service that stores failed operations for asynchronous retry.

The minimum viable implementation is a failed_operations table with columns for the operation type, payload, attempt count, last error, and next retry time. A background job (a cron worker, a queue consumer) reads the table every minute, retries each row that is due, and either marks it succeeded or increments the attempt count. Operations that exceed a max attempt count get flagged for human review, not silently dropped.

EXPLAINER DIAGRAM titled HOW FAILED OPERATIONS GET RETRIED showing a left to right flow on a slate background. Far left shows a green box labeled USER ACTION with an arrow pointing right. The arrow leads to a teal box labeled INLINE RETRY 3X with a small loop arrow. From INLINE RETRY two arrows split, one going up to a green checkmark labeled SUCCESS with a label reading 90 PERCENT OF CALLS, the other going down to an orange box labeled DEAD LETTER QUEUE with a database icon. The DEAD LETTER QUEUE has an arrow pointing right to a teal box labeled CRON WORKER EVERY 60S which has another loop arrow labeled BACKOFF EXPONENTIAL. From the CRON WORKER two arrows split, one to a green checkmark labeled SUCCESS labeled 9 PERCENT OF CALLS, the other to a red box labeled HUMAN REVIEW labeled 1 PERCENT OF CALLS. A footer reads ALMOST EVERY OPERATION SUCCEEDS, BUT THE 1 PERCENT IS WHERE TRUST IS LOST OR KEPT.
The two-tier retry pattern recovers gracefully from transient failures while ensuring genuinely broken operations get human attention instead of being silently dropped.

The dead letter queue is the difference between "we lost the email confirmation for your order" and "your email was delayed by 4 hours during an outage." Users tolerate the second outcome. They do not tolerate the first.

Pattern 5: Structured Error Responses

The last pattern is the smallest in code but the largest in debugging time saved. AI tends to generate API endpoints that return either a 200 with a success payload or a 500 with a generic message. Production needs more, structured error responses that include a stable error code, a human-readable message, and an optional debug ID.

return Response.json(
  {
    error: {
      code: 'PAYMENT_DECLINED',
      message: 'Your card was declined.',
      requestId: 'req_abc123',
    },
  },
  { status: 402 }
);

The frontend can match on error.code to show a specific UI. The backend can correlate the requestId to a specific log entry. The customer support team can give the request ID to a customer and look up exactly what happened. None of this works if the API returns "internal server error."

Common Mistake

The single most damaging error handling pattern AI generates is the silent default value, code shaped like try { return await fetch(...) } catch { return [] }. The empty array looks like "no results found" to the rest of the app, when in reality the API is down. Always log, alert, and propagate, never silently substitute.

The combination of these five patterns, timeouts, retries with backoff, idempotency keys, dead letter queues, and structured error responses, covers roughly 80% of the error handling AI omits. None of them are technically difficult. They are just invisible until something fails, and AI is not motivated to make invisible things visible.

What This Means For You

The hardest part of error handling is not the code, it is remembering to ask for it. AI will write all five patterns correctly when prompted by name. The skill is knowing the names.

  • If you're a founder: Add a checklist to your AI prompt template, "include timeouts, retries, idempotency, and structured errors." Three words save you a category of incident.
  • If you're changing careers: These patterns are the operational vocabulary that distinguishes "I can ship a feature" from "I can ship a feature that survives." Internalize them once and reuse forever.
  • If you're a student: Build a tiny demo that calls a third-party API and intentionally break each pattern, no timeout, no retry, no idempotency. Watch it fail. The lesson sticks better than reading.
Keep your error paths visible

Browse more reliability and observability guides

Read more ship guides
PJ
Pranay Joshi

20+ years building products at scale. VP of Product & Engineering, startup founder, and AI coach. Helping dreamers turn ideas into reality with vibe coding.

The Tuesday Shipping Report

Every Tuesday, one focused email:

  • - The tool or technique that's actually working right now
  • - A real problem from the community (and how to solve it)
  • - What changed this week in the vibe coding landscape

Read by 1,000+ founders, developers, and creators building with AI. Free forever. No spam.