Why AI-Generated Error Handling Fails in Production (and How to Fix It) | Deployxa

AI assistants write error handling that works in testing but crashes in production. Here are the 6 reasons and the production checklist to fix them.

← Back to Dispatch Articles
Engineering Log

Why AI-Generated Error Handling Fails in Production (and How to Fix It)

AI assistants write error handling that works in testing but crashes in production. Here are the 6 reasons and the production checklist to fix them.

Why AI-Generated Error Handling Fails in Production

Error handling is the difference between an app that degrades gracefully and an app that crashes. AI assistants generate error handling that works in testing (where errors are rare and predictable) but fails in production (where errors are common and unpredictable). The result is apps that crash on unexpected errors, swallow errors silently, leak sensitive information in error messages, and provide no visibility into failures. Here are the 6 reasons AI-generated error handling fails in production, and the production checklist to fix each one.

The direct answer is that error handling is a deceptively complex topic that AI assistants get wrong in predictable ways. The 6 reasons are: swallowed errors, no logging, no retry logic, no circuit breaker, information leakage, and no user-friendly error messages. Each one has a known cause and a known fix, and applying all 6 fixes gives you a production-ready error handling system. For more on why AI apps fail in production, see our article on why AI apps break on the first real user.

Reason 1: Swallowed Errors

The most common reason AI-generated error handling fails is swallowed errors. AI assistants often write try { ... } catch (err) { console.log(err); } or even try { ... } catch (err) { } (empty catch block), which swallows the error without handling it. The error is logged (or not) and the app continues, but the underlying issue is not addressed. In production, this means errors accumulate silently, data gets corrupted, and users see broken behavior with no indication of why. The fix is to handle errors explicitly: log them with context (request ID, user ID, stack trace), return a clear error response to the user, and (if appropriate) retry the operation or fail gracefully. Never swallow an error without at least logging it.

Reason 2: No Logging

The second reason is no logging. AI assistants often use console.log(err) for error logging, which is insufficient for production. console.log does not include context (request ID, user ID, timestamp), it does not support log levels (info, warn, error), and it does not integrate with log management systems. The fix is to use a structured logger (e.g., pino for Node.js, structlog for Python, slog for Go) that outputs JSON logs with context. A good error log entry includes: timestamp, log level (error), error message, stack trace, request ID, user ID, and any other relevant context. This makes errors searchable, filterable, and analyzable, which is essential for debugging production issues. For more on observability, see our article on debugging from your IDE with deployxa doctor.

Reason 3: No Retry Logic

The third reason is no retry logic. In production, many errors are transient: a database connection fails because of a network blip, an external API returns 503 because of a temporary overload, a file upload fails because of a DNS hiccup. AI assistants rarely add retry logic, which means these transient errors cause request failures, even though a retry would succeed. The fix is to add retry logic for operations that can fail transiently: use exponential backoff (e.g., 1 second, 2 seconds, 4 seconds) to avoid overwhelming the failing service, set a maximum retry count (e.g., 3) to avoid infinite loops, and only retry idempotent operations (to avoid duplicate side effects). For Node.js, use axios-retry or p-retry. For Python, use tenacity.

Reason 4: No Circuit Breaker

The fourth reason is no circuit breaker. When an external service (e.g., a payment API, a database) fails, retrying every request can make the situation worse, because you are sending more traffic to an already-failing service. A circuit breaker solves this by monitoring failures and, when the failure rate exceeds a threshold, "tripping" the circuit (stopping all requests to the failing service) for a cooldown period. After the cooldown, the circuit breaker allows a test request; if it succeeds, the circuit is reset, and if it fails, the cooldown is extended. AI assistants rarely add circuit breakers, which means failing services can cascade and take down the entire app. The fix is to use a circuit breaker library (e.g., opossum for Node.js, pybreaker for Python) for all external service calls.

Reason 5: Information Leakage

The fifth reason is information leakage. AI assistants often return raw error messages to the user, which can leak sensitive information (e.g., database connection strings, file paths, stack traces). This is a security risk, because attackers can use this information to probe your app's vulnerabilities. The fix is to never return raw error messages to the user. Instead, return a generic error message (e.g., "Something went wrong. Please try again.") and log the full error internally. For 404 errors, return "Resource not found." For 401 errors, return "Unauthorized." For 500 errors, return "Internal server error." Never include stack traces, file paths, or database details in user-facing error messages.

Reason 6: No User-Friendly Error Messages

The sixth reason is no user-friendly error messages. AI assistants often return technical error messages (e.g., "ERESOLVE unable to resolve dependency tree") that are meaningful to developers but not to users. The fix is to translate errors into user-friendly messages: "We could not process your request. Please try again." or "Your session has expired. Please log in again." The user-friendly message should tell the user what went wrong (in plain English) and what they can do about it (retry, contact support, etc.). The technical error should be logged internally for debugging. For more on user-friendly communication, see our article on the heuristic advisor, which translates build errors into plain English.

Step-by-Step: Implementing Production-Ready Error Handling

Here is the exact workflow for implementing production-ready error handling in a typical Express app.

Step 1: Install a structured logger

npm install pino pino-http

Step 2: Configure the logger

const pino = require('pino');
const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  redact: ['req.headers.authorization', 'req.body.password'], // redact sensitive fields
});

app.use(require('pino-http')({ logger }));

Step 3: Implement a global error handler

app.use((err, req, res, next) => {
  // Log the error with context
  req.log.error({
    err: {
      message: err.message,
      stack: err.stack,
      code: err.code,
    },
    user: req.user?.id,
    url: req.url,
    method: req.method,
  }, 'Unhandled error');
  
  // Return a user-friendly error message
  if (err.code === 'VALIDATION_ERROR') {
    return res.status(400).json({ error: 'Invalid input. Please check your data and try again.' });
  }
  if (err.code === 'NOT_FOUND') {
    return res.status(404).json({ error: 'Resource not found.' });
  }
  if (err.code === 'UNAUTHORIZED') {
    return res.status(401).json({ error: 'Your session has expired. Please log in again.' });
  }
  
  // Generic error for everything else
  res.status(500).json({ error: 'Something went wrong. Please try again.' });
});

Step 4: Add retry logic for external calls

const axios = require('axios');
const axiosRetry = require('axios-retry');

axiosRetry(axios, {
  retries: 3,
  retryDelay: (retryCount) => Math.pow(2, retryCount) * 1000, // 1s, 2s, 4s
  retryCondition: (error) => error.response?.status >= 500,
});

Step 5: Add a circuit breaker

npm install opossum
const circuitBreaker = require('opossum');

const breaker = circuitBreaker(externalApiCall, {
  timeout: 5000,
  errorThresholdPercentage: 50,
  resetTimeout: 30000,
});

breaker.fallback(() => ({ error: 'Service temporarily unavailable' }));
breaker.on('open', () => logger.warn('Circuit breaker opened'));
breaker.on('close', () => logger.info('Circuit breaker closed'));

app.get('/data', async (req, res) => {
  const result = await breaker.fire(req.params.id);
  res.json(result);
});

Step 6: Add process-level error handlers

process.on('uncaughtException', (err) => {
  logger.error({ err }, 'Uncaught exception');
  process.exit(1); // Exit and let the process manager restart
});

process.on('unhandledRejection', (reason) => {
  logger.error({ reason }, 'Unhandled rejection');
  process.exit(1);
});

Step 7: Verify with deployxa doctor

Run deployxa doctor to verify your app's health. The 14-point readiness engine checks SSL, DNS, environment variables, health endpoints, and container status. For more on the readiness engine, see our article on the 14-point readiness engine.

Common Pitfalls and Troubleshooting

The first pitfall is over-logging. Logging every error with full context can produce a huge volume of logs, which makes it hard to find important errors. The fix is to use log levels (info, warn, error) and to filter by level when searching. The second pitfall is under-logging. Not logging errors means you have no visibility into failures, which makes debugging impossible. The fix is to log every error with at least a message and a stack trace. The third pitfall is logging sensitive information. Error messages can contain passwords, tokens, and other secrets, which should never be logged. The fix is to use a logger that supports redaction (like pino) and to redact sensitive fields. The fourth pitfall is not testing error handling. Error handling code is rarely tested, which means it might not work when needed. The fix is to write tests that trigger errors and verify the error handling works correctly. The fifth pitfall is not monitoring error rates. A sudden spike in error rate can indicate a production issue, and you need to be alerted. The fix is to set up alerts on error rate (e.g., via Deployxa's scheduled health checks or an external monitoring service).

Production Hardening for Error Handling

Beyond the 6 fixes, error handling systems benefit from several additional hardening steps. The first is error tracking. Use an error tracking service (e.g., Sentry, Bugsnag) that captures unhandled errors, groups them, and alerts you when new errors occur. This gives you visibility into production errors that you would not otherwise see. The second is health check endpoints. A /health endpoint that returns a 200 status code when the app is healthy is essential for production. The Deployxa readiness engine checks this endpoint, and the load balancer uses it to determine whether to route traffic to the container. The third is graceful degradation. When a non-critical service fails (e.g., the email service), the app should degrade gracefully (e.g., queue the email for later) rather than failing the entire request. The fourth is timeout management. All external calls (e.g., database, API) should have timeouts, to prevent the app from hanging indefinitely if the external service is slow. The fifth is backpressure. If the app is overloaded (e.g., high request rate), it should return a 503 (Service Unavailable) status code rather than accepting requests it cannot handle, which prevents cascading failures. For more on production hardening, see our articles on the 14-point readiness engine and debugging from your IDE.

When Error Handling Over-Engineering Is a Problem

While robust error handling is essential, over-engineering error handling can be a problem. Adding retry logic, circuit breakers, and fallbacks to every operation adds complexity and can mask underlying issues. The fix is to apply error handling patterns selectively: use retries for transient failures (e.g., network blips), use circuit breakers for external services (e.g., APIs), and use fallbacks for non-critical features (e.g., recommendations). For internal operations (e.g., database queries), simple error handling (log and return a 500) is often sufficient. The key is to match the error handling to the failure mode, not to apply every pattern everywhere. For more on error handling, see our articles on the heuristic advisor and the agentic incident response pipeline.

Conclusion: Error Handling Is Not Optional

The error handling gap is not a sign that your AI assistant did a bad job. It is a sign that error handling is deceptively complex, and AI assistants generate code that works in testing but fails in production. By applying the 6 fixes above (handle errors explicitly, log with context, retry transient failures, use circuit breakers, avoid information leakage, provide user-friendly messages), you can build a production-ready error handling system that degrades gracefully and provides visibility into failures. Stop shipping fragile apps and start handling errors properly.

Ready to deploy with robust error handling? Drag your project to Deployxa Drop for an instant live preview, or install the CLI with npm i -g @deployxa/cli and deploy from your terminal. For more on AI coding patterns, see our articles on the file upload trap and the JWT authentication trap. Learn about the rate limiting gap and why AI apps break on mobile in our companion articles. Explore our free developer tools to speed up your workflow.

Ready to deploy with Deployxa?

Deploy your apps globally with automatic SSL and AI diagnostics.

Start Free Now