API Error Handling: Do It Right

Most APIs have terrible error handling. Here's how to build error responses that actually help developers.

The Problem

You hit an API and get back:

{ "error": "Something went wrong" }

Gee, thanks. Very helpful.

Good Error Responses

Include:

1. **Error code** - Machine-readable identifier 2. **Message** - Human-readable explanation 3. **Details** - What specifically failed 4. **Documentation link** - Where to learn more

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request body",
    "details": [
      {
        "field": "email",
        "issue": "must be a valid email address",
        "received": "not-an-email"
      }
    ],
    "documentation": "https://api.example.com/docs/errors#VALIDATION_ERROR"
  }
}

HTTP Status Codes Matter

Use them correctly:

  • `400` - Client error, fix your request
  • `401` - Not authenticated, log in
  • `403` - Authenticated but not authorized
  • `404` - Resource not found
  • `422` - Validation failed
  • `429` - Rate limited
  • `500` - Server error, our fault

Implementation in Express

class AppError extends Error {
  constructor(code, message, statusCode = 400, details = null) {
    super(message);
    this.code = code;
    this.statusCode = statusCode;
    this.details = details;
  }
}

// Usage
throw new AppError(
  'VALIDATION_ERROR',
  'Invalid request body',
  422,
  [{ field: 'email', issue: 'required' }]
);

// Error handler middleware
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  
  res.status(statusCode).json({
    error: {
      code: err.code || 'INTERNAL_ERROR',
      message: err.message,
      details: err.details,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
  });
});

Consistent Error Format

Every error, everywhere, same shape. Your API consumers will thank you.

Frontend can now:

try {
  await api.createUser(data);
} catch (err) {
  if (err.code === 'VALIDATION_ERROR') {
    // Show field-specific errors
    err.details.forEach(({ field, issue }) => {
      showFieldError(field, issue);
    });
  } else {
    // Generic error handling
    showToast(err.message);
  }
}

Predictable errors make for better developer experience.

Back to Journal