Difficulty: Advanced
Proper error handling in REST APIs is critical for both the developer experience and application reliability. Clients need consistent, informative error responses to handle failures gracefully. Flask provides several mechanisms for error handling that you should use in every API.
Flask's abort() function immediately stops request processing and returns an error response. By default, it returns HTML error pages, which is not ideal for APIs. Custom error handlers let you override this behavior to return JSON responses with meaningful error details.
A well-designed API error response should include: an error code or type, a human-readable message, optional details about what went wrong, and the HTTP status code. Some APIs also include a request ID for debugging, a documentation URL, and field-level validation errors.
Custom exception classes give you structured error handling. Define exception classes that map to specific HTTP errors (BadRequest, NotFound, Forbidden). Each carries a status code and message. Register error handlers for these exceptions to convert them into JSON responses.
Flask's @app.errorhandler can handle both status codes (404, 500) and exception classes (ValueError, CustomError). For APIs, register handlers for common HTTP errors and your custom exceptions. Use a generic 500 handler as a safety net to catch unexpected errors.
Logging is essential for debugging errors in production. Flask uses Python's logging module. Configure it to log to files, external services (Sentry, CloudWatch), or stdout (for Docker). Log the error details, request information, and stack trace for 500 errors. Never expose stack traces to API clients.
For validation errors (wrong field types, missing required fields, invalid values), return 400 or 422 with a structured response listing each field and its errors. This helps frontend developers build proper error displays. Libraries like marshmallow provide validation with structured error output.
from flask import Flask, jsonify
app = Flask(__name__)
@app.errorhandler(400)
def bad_request(error):
return jsonify({
'error': 'Bad Request',
'message': str(error.description),
'status': 400
}), 400
@app.errorhandler(404)
def not_found(error):
return jsonify({
'error': 'Not Found',
'message': 'The requested resource was not found.',
'status': 404
}), 404
@app.errorhandler(405)
def method_not_allowed(error):
return jsonify({
'error': 'Method Not Allowed',
'message': str(error.description),
'status': 405
}), 405
@app.errorhandler(500)
def server_error(error):
app.logger.error(f'Server Error: {error}')
return jsonify({
'error': 'Internal Server Error',
'message': 'An unexpected error occurred.',
'status': 500
}), 500
Error handlers convert Flask's default HTML error pages to JSON. The 500 handler logs the error but returns a generic message to the client (never expose stack traces). Register handlers for all common HTTP errors.
from flask import Flask, jsonify
class APIError(Exception):
"""Base class for API errors."""
def __init__(self, message, status_code=400, details=None):
self.message = message
self.status_code = status_code
self.details = details
def to_dict(self):
rv = {'error': self.message, 'status': self.status_code}
if self.details:
rv['details'] = self.details
return rv
class NotFoundError(APIError):
def __init__(self, resource='Resource'):
super().__init__(f'{resource} not found', 404)
class ValidationError(APIError):
def __init__(self, errors):
super().__init__('Validation failed', 422, details=errors)
app = Flask(__name__)
@app.errorhandler(APIError)
def handle_api_error(error):
return jsonify(error.to_dict()), error.status_code
@app.get('/api/users/<int:user_id>')
def get_user(user_id):
user = User.query.get(user_id)
if not user:
raise NotFoundError('User')
return jsonify(user.to_dict())
@app.post('/api/users')
def create_user():
data = request.get_json()
errors = {}
if not data.get('name'):
errors['name'] = 'Name is required'
if not data.get('email'):
errors['email'] = 'Email is required'
if errors:
raise ValidationError(errors)
# Create user...
Custom exception classes carry error information. A base APIError class provides status code and message. Subclasses like NotFoundError and ValidationError add semantic meaning. One error handler catches all APIError subclasses.
import logging
from flask import Flask, jsonify
app = Flask(__name__)
# Configure logging
if not app.debug:
# File handler for production
file_handler = logging.FileHandler('app.log')
file_handler.setLevel(logging.WARNING)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
app.logger.addHandler(file_handler)
@app.errorhandler(Exception)
def handle_unexpected_error(error):
"""Catch-all for unhandled exceptions."""
app.logger.exception(f'Unhandled exception: {error}')
return jsonify({
'error': 'Internal Server Error',
'message': 'Something went wrong. Please try again later.',
}), 500
@app.route('/api/data')
def get_data():
app.logger.info('Data endpoint accessed')
try:
result = perform_operation()
return jsonify(result)
except ValueError as e:
app.logger.warning(f'ValueError in get_data: {e}')
return jsonify({'error': str(e)}), 400
Configure file logging for production. The catch-all Exception handler logs stack traces with logger.exception() but returns a generic message to clients. Use different log levels (info, warning, error) appropriately.
from flask import Flask, request, jsonify
app = Flask(__name__)
def validate_user_data(data):
"""Validate user creation data and return errors dict."""
errors = {}
if not data:
return {'_general': 'Request body must be JSON'}
if not data.get('name') or len(data['name'].strip()) < 2:
errors['name'] = 'Name must be at least 2 characters'
email = data.get('email', '')
if not email or '@' not in email:
errors['email'] = 'Valid email is required'
age = data.get('age')
if age is not None and (not isinstance(age, int) or age < 0):
errors['age'] = 'Age must be a positive integer'
return errors
@app.post('/api/users')
def create_user():
data = request.get_json(silent=True)
errors = validate_user_data(data)
if errors:
return jsonify({
'error': 'Validation failed',
'status': 422,
'details': errors
}), 422
# Proceed with creation
return jsonify({'created': data['name']}), 201
Validation errors return 422 with field-level error messages. The 'details' dict maps field names to error descriptions. Clients can use this to display errors next to the corresponding form inputs.
Error Handlers, Custom Exceptions, abort, Error Response Format, Logging