AI-generated code is often correct but brittle. It works in ideal conditions but fails unexpectedly in production, where inputs vary, dependencies shift, and services degrade. Error handling in AI-generated code is not an afterthought—it is the primary mechanism for turning a smart but fragile pipeline into a resilient system. This guide details how to structure, test, and operate AI-generated code with robust error handling, covering the full lifecycle from generation to deployment and monitoring.
Handling Runtime Failures
When AI-generated code runs in production, it frequently encounters input that deviates from training data. The most common failure is a ValidationError that is not caught at the call site.
Catching and Logging Input Errors
AI-generated functions often assume input is well-formed JSON, but real-world data arrives with missing keys, malformed types, or unexpected structures.
{
"user_id": 123,
"preferences": {
"theme": "dark",
"notifications": true
},
"metadata": {
"source": "mobile_app",
"timestamp": "2025-04-05T12:34:56Z"
}
}
A common failure occurs when preferences.notifications is 1 instead of true.
def process_user_profile(raw_data: dict) -> dict:
user_id = raw_data["user_id"]
theme = raw_data["preferences"]["theme"]
notifications = raw_data["preferences"]["notifications"] # KeyError if missing
return {
"user_id": user_id,
"processed_at": datetime.now().isoformat(),
"theme": theme,
"notifications_enabled": notifications
}
This code fails silently when preferences.notifications is 1, because notifications = True is assumed, but 1 is not True in Python.
To fix this, use a try-except block with structured logging:
import logging
import json
from typing import Any, Dict
logger = logging.getLogger("user_profile_processor")
def process_user_profile(raw_data: Dict[str, Any]) -> Dict[str, Any]:
try:
user_id = raw_data["user_id"]
theme = raw_data["preferences"]["theme"]
notifications = raw_data["preferences"]["notifications"]
# Validate types
if not isinstance(notifications, bool):
raise ValueError(
f"Expected boolean for notifications, got {type(notifications).__name__}: {notifications}"
)
return {
"user_id": user_id,
"processed_at": datetime.now().isoformat(),
"theme": theme,
"notifications_enabled": notifications
}
except KeyError as e:
error_context = {
"raw_input": raw_data,
"missing_key": str(e),
"input_schema": {
"user_id": "integer",
"preferences": {
"theme": "string",
"notifications": "boolean"
}
}
}
logger.error(
"Input validation failed",
extra=error_context
)
raise
except ValueError as e:
error_context = {
"raw_input": raw_data,
"validation_error": str(e),
"expected_type": "boolean",
"actual_value": raw_data["preferences"]["notifications"]
}
logger.error(
"Type validation failed",
extra=error_context
)
raise
This structure logs the exact input, the expected schema, and the actual value, making it easy to diagnose issues in logs or monitoring dashboards.
> Key insight: The extra field in logger.error() is the single most effective tool for capturing context in AI-generated error handling. It is not just a string—it is a structured object that can be parsed by log aggregators like Loki or Fluentd.
Handling Dependency Failures
AI-generated code often calls external services: APIs, databases, message queues. These are prone to transient and permanent failures.
Retry with Exponential Backoff
When calling an external API, a simple requests.get() may fail due to network jitter, timeouts, or service unavailability.
import requests
import time
import logging
def fetch_user_data(user_id: int) -> dict:
url = f"https://api.example.com/users/{user_id}"
max_retries = 5
base_delay = 1 # seconds
backoff_factor = 2
timeout = 10
for attempt in range(max_retries):
try:
response = requests.get(url, timeout=timeout)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
logger.warning(
f"Request to {url} timed out (attempt {attempt + 1})",
extra={"attempt": attempt + 1, "url": url}
)
except requests.exceptions.ConnectionError:
logger.warning(
f"Connection error for {url} (attempt {attempt + 1})",
extra={"attempt": attempt + 1, "url": url}
)
except requests.exceptions.HTTPError as e:
logger.error(
f"HTTP error for {url}: {e.response.status_code}",
extra={"url": url, "status_code": e.response.status_code}
)
# Return response for later inspection
return e.response.json()
# Exponential backoff
delay = base_delay * (backoff_factor ** attempt)
time.sleep(delay)
# Final fallback: raise after all retries
raise RuntimeError(f"Failed to fetch user data after {max_retries} attempts")
The base_delay and backoff_factor are critical: a delay of 1, 2, 4, 8, 16 seconds ensures that transient network issues are absorbed without overloading the system.
> Silent failure: If the requests.get() call does not raise HTTPError for 4xx or 5xx responses, the function may succeed on a 404 but return an empty dict, which is not an error. Use response.raise_for_status() to catch all non-2xx responses.
Handling Data Pipeline Errors
AI-generated code often orchestrates a sequence of operations: fetch, transform, validate, store.
Pipeline with Error Recovery and Checkpoints
In a data pipeline, a single failure can cascade through downstream systems.
def run_data_pipeline(batch_id: str) -> dict:
logs = []
try:
# Step 1: Fetch raw data
raw_data = fetch_raw_data(batch_id)
logs.append({"step": "fetch", "status": "success", "count": len(raw_data)})
# Step 2: Transform
processed_data = []
for item in raw_data:
try:
transformed = transform_item(item)
processed_data.append(transformed)
except Exception as e:
logs.append({
"step": "transform",
"status": "failed",
"item_id": item.get("id"),
"error": str(e)
})
continue # proceed with other items
# Step 3: Validate
validation_results = validate_batch(processed_data)
logs.append({
"step": "validate",
"status": "success",
"valid_count": validation_results["valid"],
"invalid_count": validation_results["invalid"]
})
# Step 4: Store
store_results(processed_data)
logs.append({"step": "store", "status": "success"})
return {
"batch_id": batch_id,
"logs": logs,
"status": "completed"
}
except Exception as e:
# Final error: pipeline failed
logs.append({
"step": "pipeline",
"status": "failed",
"error": str(e)
})
raise
This structure allows partial success: if one item fails during transform_item, the pipeline continues and logs the failure without stopping.
> Critical detail: The continue in the transform_item loop is essential. Without it, a single malformed record can halt the entire batch. The logs list becomes a first-class artifact, available for audit and debugging.
Handling Configuration and Environment Errors
AI-generated code often relies on configuration files, environment variables, and secrets. Misconfiguration is a common cause of silent failures.
Loading Configuration with Validation
import json
import os
from pathlib import Path
def load_config(config_path: Path) -> dict:
try:
with open(config_path, "r") as f:
raw_config = json.load(f)
except FileNotFoundError:
error_msg = f"Configuration file not found: {config_path}"
logger.error(error_msg)
raise RuntimeError(error_msg)
# Required keys
required_keys = ["api_base_url", "database_url", "batch_size", "workers"]
missing_keys = [k for k in required_keys if k not in raw_config]
if missing_keys:
error_msg = f"Missing required config keys: {', '.join(missing_keys)}"
logger.error(error_msg, extra={"missing_keys": missing_keys})
raise RuntimeError(error_msg)
# Type validation
if not isinstance(raw_config["batch_size"], int):
error_msg = f"Expected batch_size to be integer, got {type(raw_config['batch_size']).__name__}"
logger.error(error_msg, extra={"batch_size": raw_config["batch_size"]})
raise ValueError(error_msg)
if not isinstance(raw_config["workers"], int) or raw_config["workers"] < 1:
error_msg = "Workers must be a positive integer"
logger.error(error_msg, extra={"workers": raw_config["workers"]})
raise ValueError(error_msg)
return raw_config
> Sharp edge: os.getenv("DATABASE_URL") returns a string, but json.load expects a file. A common mistake is to pass os.getenv("DATABASE_URL") directly to json.load, which fails silently because strings are not file-like objects.
Environment Variable Defaults and Overrides
import os
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://localhost:5432/app")
BATCH_SIZE = int(os.getenv("BATCH_SIZE", "100"))
WORKERS = int(os.getenv("WORKERS", "4"))
# Validate
if not DATABASE_URL:
raise RuntimeError("DATABASE_URL is required")
if BATCH_SIZE < 1:
raise ValueError("BATCH_SIZE must be at least 1")
if WORKERS < 1:
raise ValueError("WORKERS must be at least 1")
> Confusion point: int(os.getenv("BATCH_SIZE", "100")) defaults to 100, but os.getenv("BATCH_SIZE") returns None if the variable is not set. This leads to BATCH_SIZE = 0 when BATCH_SIZE is set to the empty string. Use int(os.getenv("BATCH_SIZE", "100") or "100") to avoid this.
Monitoring and Observability
Structured Error Logging with Context
Use structured logging with consistent keys across all error handlers.
logger.error(
"Failed to process user profile",
extra={
"event_id": "user_profile_123",
"user_id": 123,
"input_schema": {
"user_id": "integer",
"preferences": {
"theme": "string",
"notifications": "boolean"
}
},
"error_type": "ValidationError",
"timestamp": datetime.now().isoformat(),
"trace_id": "abc123def456"
}
)
This format enables filtering, alerting, and correlation across services. Tools like Grafana, Elasticsearch, or Prometheus can extract user_id, error_type, and trace_id for drill-down.
> Production pitfall: Logs are written, but no alerting is configured. Use log level and error_type to trigger alerts. For example, alert when error_type is ValidationError and count > 10 in 5 minutes.
Summary of Best Practices
- Use
try-exceptwithextracontext in logs for every critical function. - Always validate input schema and types before processing.
- Implement retry with exponential backoff for external dependencies.
- Design pipelines for partial success: continue on error, log failures, return summary.
- Validate configuration files and environment variables thoroughly.
- Use structured logging with consistent keys across services.
- Do not assume defaults: explicitly set and validate them.
Error handling is not a layer on top of AI-generated code—it is the foundation. When the AI writes the code, it is the engineer’s job to make it resilient.
This page was rewritten on 11 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
Join the Newsletter
Get the latest updates and deep insights shipped directly to your inbox.