API Error Handling: What to Do When Your Backend Breaks
Your AI-generated API works perfectly — until someone sends a bad request, the database goes offline, or an edge case you never thought of blows the whole thing up. Here's how to build in the safety rails.
TL;DR
AI generates API routes that work on the "happy path" — when everything goes right. But real-world traffic includes missing fields, invalid data, database outages, and weird edge cases. Without error handling, one bad request can crash your entire server. Error handling is the safety rail that keeps your backend running when things go sideways. This article shows you what AI typically gets wrong, what proper error handling looks like, and exactly how to prompt AI to add it.
Why AI Coders Need to Understand Error Handling
Here's a scenario every vibe coder has lived through: You ask Claude or ChatGPT to build an API route. It generates clean, working code. You test it with good data. It works perfectly. You deploy it. Then a real user sends a request with a missing field, or your database hiccups for half a second, and your entire application crashes with a 500 Internal Server Error.
No useful error message. No indication of what went wrong. Just a dead server and confused users.
If you've worked construction, think about it this way: API error handling is the safety protocol for your backend. You wouldn't let workers on a job site without hard hats and safety rails — not because falls happen every day, but because when they do happen, the consequences are serious. Error handling works the same way. It doesn't prevent problems, but it makes sure one problem doesn't take down the whole operation.
A 2025 Postman survey found that 73% of API failures in production stem from inadequate error handling — not from bugs in the core logic. The code works fine. It just doesn't know what to do when something unexpected happens.
AI coding tools are particularly bad at this because they optimize for the question you asked. You said "build me a user registration route." The AI built exactly that — a route that registers users. You didn't say "and handle every possible thing that could go wrong," so it didn't.
That's not the AI's fault. It's a gap in the prompt. And this article teaches you how to close it.
The Real Scenario: Your API Route Crashes with a 500 Error
"Build me an Express API route that gets a user by their ID from the database and returns their profile."
Sounds reasonable. And the AI delivers a clean-looking route. You test it with a valid user ID — works great. Then someone hits the route with an ID that doesn't exist. Or sends text instead of a number. Or the database connection drops for a second. Boom — 500 Internal Server Error.
Your users see a generic error page. Your server logs are a wall of red. And you have no idea where to start debugging because the error message just says "Internal Server Error" with no details.
Let's look at exactly what AI generated, why it breaks, and how to fix it.
What AI Generated: The Happy-Path-Only Version
Here's the kind of code AI typically generates when you ask for a basic API route. It works — but only when everything goes perfectly:
// ❌ No error handling — works fine until it doesn't
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
res.json(user.rows[0]);
});
This code does exactly what you asked for. It takes a user ID from the URL, queries the database, and returns the result. But here's what goes wrong:
- User doesn't exist:
user.rows[0]isundefined. The route sends backundefinedas a response — or worse, crashes when later code tries to access a property on it. - Database is down: The
db.querycall throws an error. There's nocatchto handle it. Your entire server crashes. - Invalid ID format: Someone sends
/api/users/abcinstead of a number. The database query fails with a type error. - Missing ID: Edge cases in URL parsing could result in an empty ID hitting the query.
Every single one of these causes a 500 Internal Server Error. Your users get no useful information. You get no useful logs. The server might even crash entirely and take every other user's session down with it.
It's like running a job site with no safety plan. Things work fine 95% of the time. But that 5% can shut down the entire operation.
The Proper Version: With Error Handling
Here's the same route with proper error handling. This is what you want AI to generate — and what you should ask for explicitly:
// ✅ Proper error handling — catches crashes, sends useful responses
app.get('/api/users/:id', async (req, res) => {
try {
// Validate input first
const userId = parseInt(req.params.id, 10);
if (isNaN(userId)) {
return res.status(400).json({
error: 'Invalid user ID',
message: 'User ID must be a number'
});
}
// Query the database
const result = await db.query(
'SELECT id, name, email FROM users WHERE id = $1',
[userId]
);
// Handle "not found" case
if (result.rows.length === 0) {
return res.status(404).json({
error: 'User not found',
message: `No user exists with ID ${userId}`
});
}
// Success — return the user
res.json(result.rows[0]);
} catch (error) {
// Log the REAL error (server-side only)
console.error('Error fetching user:', error);
// Send a GENERIC message to the client
res.status(500).json({
error: 'Internal server error',
message: 'Something went wrong. Please try again later.'
});
}
});
Same route. Same functionality. But now it handles every scenario that could go wrong. Let's break down each piece.
Understanding Each Part
Try/Catch: The Safety Rails
Try/catch is the most fundamental error handling pattern you'll see in AI-generated code. Think of it as the safety rails on a scaffolding: the try block is where the work happens, and the catch block is the rail that keeps everything from falling to the ground when something slips.
try {
// Your code runs here — the "normal" path
// If anything in here throws an error, execution
// immediately jumps to the catch block
} catch (error) {
// This runs ONLY if something went wrong
// The "error" variable contains details about what happened
}
Without try/catch, an error in your API route is like a worker falling off scaffolding with no safety net. The error "falls" all the way through your application, crashes the server process, and every user connected to that server loses their session. With try/catch, the fall is caught. The error is handled. The server keeps running.
Status Codes: Telling the Client What Happened
HTTP status codes are how your server communicates what happened to whoever made the request. They're like the colored tags on an inspection — green means pass, yellow means warning, red means stop.
Here are the ones you'll use most in error handling:
| Code | Name | What It Means | When to Use It |
|---|---|---|---|
200 |
OK | Everything worked | Successful requests |
400 |
Bad Request | The client sent something invalid | Missing fields, wrong data types |
401 |
Unauthorized | No valid authentication | Missing or expired login token |
403 |
Forbidden | Authenticated but not allowed | User doesn't have permission |
404 |
Not Found | The thing you asked for doesn't exist | User ID not in database, wrong URL |
500 |
Internal Server Error | The server crashed | Unhandled errors, database failures |
The key distinction: 4xx errors mean the client did something wrong. 5xx errors mean the server did something wrong. Your error handling should always use the most specific status code possible. If someone sends an invalid email format, that's a 400, not a 500. If a user isn't found, that's a 404, not a 500.
When AI doesn't add error handling, everything that goes wrong returns 500 — even when it's the user's fault. That makes debugging nearly impossible.
Error Messages: What to Say and What to Hide
Error messages have two audiences: the user (who needs to know what to do) and you, the developer (who needs to know what broke).
// What the user sees (clean, helpful, safe)
res.status(400).json({
error: 'Invalid email',
message: 'Please provide a valid email address'
});
// What gets logged on the server (detailed, for debugging)
console.error('Validation failed:', {
field: 'email',
value: req.body.email,
reason: 'Failed regex validation',
timestamp: new Date().toISOString()
});
The user gets a friendly, actionable message. You get the full details in your server logs. Never expose internal details to users. Stack traces, database table names, file paths, SQL queries — all of that is server-side only. Exposing it is a security risk and tells attackers exactly how your system is built.
Error Types: Not All Errors Are Equal
Proper error handling treats different errors differently. Here are the main categories:
- Validation errors (400): The user sent bad data. Missing required fields, wrong types, values out of range. These are expected and should have specific, helpful messages.
- Authentication errors (401): The request doesn't include valid credentials. Token expired, no token sent, invalid API key.
- Not found errors (404): The requested resource doesn't exist. User ID that isn't in the database, endpoint that doesn't exist.
- Server errors (500): Something unexpected broke. Database connection dropped, external API timed out, a bug in your code. These should log the full error and send a generic message to the client.
Good error handling catches each type and responds appropriately. Bad error handling treats everything the same — either crashing silently or returning a generic "something went wrong" for every problem.
What AI Gets Wrong About Error Handling
AI coding tools have three consistent bad habits with error handling. Watch for all three in any code you generate.
1. Catching Errors Silently
This is the most common and most dangerous pattern:
// ❌ Silent catch — the error vanishes into thin air
app.post('/api/orders', async (req, res) => {
try {
const order = await createOrder(req.body);
res.json(order);
} catch (error) {
// This catches the error... and does nothing with it
res.json({ success: false });
}
});
The error is caught — so the server doesn't crash. But nothing is logged. The response doesn't tell the client what went wrong or what status code applies. You can't debug it because there's no record of what happened. It's like a safety inspector who sees a violation, says "no comment," and walks away.
The fix: Always log the error and always send a meaningful status code:
// ✅ Catches, logs, and responds properly
} catch (error) {
console.error('Order creation failed:', error);
res.status(500).json({
error: 'Order creation failed',
message: 'Unable to create your order. Please try again.'
});
}
2. Exposing Internal Error Details
The opposite extreme — AI sends the raw error straight to the user:
// ❌ Sends the entire error object to the client
} catch (error) {
res.status(500).json({
error: error.message,
stack: error.stack,
query: 'SELECT * FROM users WHERE...'
});
}
This is a security nightmare. You've just told the world what database you're using, what your queries look like, and the exact file paths on your server. An attacker could use this information to craft targeted attacks against your database or file system.
The fix: Send generic messages to clients, log details server-side:
// ✅ Generic for client, detailed for your logs
} catch (error) {
console.error('Database query failed:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Something went wrong. Please try again later.'
});
}
3. Not Validating Input
AI often skips input validation entirely, trusting that whatever the user sends is correct:
// ❌ No validation — trusts the client completely
app.post('/api/users', async (req, res) => {
const { name, email, age } = req.body;
// What if name is missing? Email is invalid? Age is -5?
await db.query(
'INSERT INTO users (name, email, age) VALUES ($1, $2, $3)',
[name, email, age]
);
res.json({ success: true });
});
Without validation, garbage data gets into your database. Missing required fields cause crashes. Invalid email formats break your email sending. Negative ages, 500-character names, SQL injection attempts — all of it passes through unchecked.
The fix: Validate before you process:
// ✅ Validate input before doing anything with it
app.post('/api/users', async (req, res) => {
try {
const { name, email, age } = req.body;
// Check required fields
if (!name || !email) {
return res.status(400).json({
error: 'Missing required fields',
message: 'Name and email are required'
});
}
// Check types and ranges
if (typeof age !== 'number' || age < 0 || age > 150) {
return res.status(400).json({
error: 'Invalid age',
message: 'Age must be a number between 0 and 150'
});
}
// Now it's safe to proceed
await db.query(
'INSERT INTO users (name, email, age) VALUES ($1, $2, $3)',
[name, email, age]
);
res.status(201).json({ success: true });
} catch (error) {
console.error('User creation failed:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Unable to create user. Please try again.'
});
}
});
How to Debug API Errors
When your API breaks — and it will break — here's how to figure out what went wrong without pulling your hair out.
Step 1: Read the Error Response
Start with what the API actually sent back. If you're testing in Postman, Insomnia, or your browser's developer tools, look at two things:
- The status code: Is it a
400(your request was bad),404(thing not found), or500(server crashed)? - The response body: If your error handling is set up properly, this should tell you exactly what went wrong.
// A well-structured error response tells you what to fix
{
"error": "Invalid user ID",
"message": "User ID must be a number"
}
// A bad error response tells you nothing
{
"error": "Internal Server Error"
}
If you're getting generic 500 errors for everything, that's your first clue: your error handling isn't specific enough. Time to go back to the code and add proper validation and specific status codes.
Step 2: Check Your Server Logs
The real debugging gold is in your server logs, not in the response. If you followed the pattern above — console.error() with details, generic message to the client — your server logs will have the full error:
# In your terminal where the server is running, you'll see:
Error fetching user: Error: connection refused at 127.0.0.1:5432
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16)
This tells you the database connection was refused on port 5432. That's a PostgreSQL port. So the database is either down or the connection details are wrong. You'd never send this to a user, but it tells you exactly where to look.
Step 3: Ask AI to Help Debug
Once you have the error details, you can ask AI to help you debug. Copy the error message from your logs and paste it into your prompt:
"My Express API route returns a 500 error. The server log shows: Error: connection refused at 127.0.0.1:5432. Here's my route code: [paste code]. What's causing this and how do I fix it?"
The more specific your error information, the better AI can help. "My API is broken" gets you a generic answer. A specific error message with the relevant code gets you a targeted fix.
Step 4: Test the Edge Cases
After fixing the immediate error, test the scenarios that break things:
- Send a request with missing required fields
- Send invalid data types (text where a number is expected)
- Request a resource that doesn't exist (invalid ID)
- Send an empty request body
- Send extremely long strings or special characters
Each test should return a specific, useful error message — not a 500. If any of them crash the server, you've found another gap in your error handling.
The Prompt That Gets Proper Error Handling
Here's the prompt template that consistently gets AI to generate proper error handling. Copy this and adapt it for your routes:
"Build an Express [GET/POST/PUT/DELETE] route for [describe what it does]. Include:
- Input validation for all required fields — return 400 with specific messages for invalid data
- Try/catch around all database calls and external API calls
- 404 response if the requested resource doesn't exist
- 500 response for unexpected errors with generic client message
- console.error() logging with full error details for every catch block
- Don't catch errors silently — every catch must log AND respond
- Don't expose internal error details, stack traces, or database info to the client"
That's specific. That covers the three mistakes AI makes. And it produces production-ready code instead of happy-path-only code.
What to Learn Next
Error handling is one piece of building reliable APIs. Here's what builds on what you just learned:
Frequently Asked Questions
Why does my API return a 500 error?
A 500 error means something crashed on the server side. Your code hit an unexpected problem — like trying to read from a database that's offline, accessing a property on undefined, or dividing by zero. Without error handling, your server doesn't know what to do with the crash, so it sends back a generic 500 Internal Server Error. Adding try/catch blocks around your route logic lets you catch these crashes and send back a useful error message instead.
What's the difference between a 400 error and a 500 error?
A 400 error means the client (the person or app making the request) sent something wrong — like missing a required field or sending invalid data. A 500 error means the server crashed while trying to handle the request. Think of it this way: 400 = "you messed up," 500 = "I messed up." Your error handling should distinguish between these so users know whether to fix their request or try again later. Learn more about all the codes in our HTTP status codes guide.
Should I show detailed error messages to users?
Never in production. Detailed error messages (like stack traces, database queries, or file paths) can expose your server's internal structure to attackers. In development, detailed errors help you debug. In production, send a friendly, generic message to the user and log the detailed error on the server side where only you can see it. A common pattern is using an environment variable like NODE_ENV to toggle between detailed (development) and generic (production) error responses.
How do I ask AI to add error handling to my API?
Be specific in your prompt. Instead of "add error handling," say: "Add try/catch to this route. Validate that the request body includes name and email. Return 400 for missing fields, 404 if the user isn't found, and 500 for unexpected errors. Log the full error server-side but only send a generic message to the client. Don't catch errors silently." This gives AI clear instructions for every error scenario instead of letting it decide what "error handling" means.
What is try/catch and why does my API need it?
Try/catch is like safety rails on a construction site. The try block is where your code runs normally. If something goes wrong — a database call fails, a value is missing, an external API is down — the catch block kicks in and handles the problem instead of letting your entire server crash. Without try/catch, one bad request can take down your whole application and every user connected to it. Read our complete try/catch guide for more.