Flask Error Handler Basics
Flask's error handling system is built around the @app.errorhandler decorator. You register a function for a specific HTTP status code or exception class, and Flask calls it whenever that condition occurs — whether triggered by abort() or raised explicitly.
from flask import Flask, jsonify, render_template
from werkzeug.exceptions import HTTPException
app = Flask(__name__)
@app.errorhandler(404)
def not_found(error):
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def internal_error(error):
return render_template('errors/500.html'), 500
The error argument passed to the handler is a Werkzeug HTTPException instance (for 4xx/5xx) or a raw Python exception (for @app.errorhandler(Exception)).
Using `abort()`
abort() raises a Werkzeug HTTPException immediately, stopping request processing:
from flask import abort
@app.route('/users/<int:user_id>')
def get_user(user_id: int):
user = db.session.get(User, user_id)
if user is None:
abort(404) # raises NotFound, triggers @errorhandler(404)
return jsonify(user.to_dict())
You can also call abort() with a Response object to customize the error body:
from flask import abort, make_response
abort(make_response(jsonify({'error': 'User not found', 'code': 'USER_404'}), 404))
Blueprint-Scoped Error Handlers
Blueprints can register their own error handlers, which take precedence over app-level handlers for requests handled by that blueprint:
# blueprints/api.py
from flask import Blueprint, jsonify
from werkzeug.exceptions import HTTPException
api = Blueprint('api', __name__, url_prefix='/api')
@api.errorhandler(404)
def api_not_found(error):
# API clients always get JSON — even for 404
return jsonify({'error': str(error), 'code': 'NOT_FOUND'}), 404
@api.errorhandler(HTTPException)
def api_http_exception(error):
# Catch all HTTP errors within the API blueprint
return jsonify({
'error': error.description,
'status': error.code,
}), error.code
Resolution order: Blueprint handler → App handler → Werkzeug default. If a blueprint does not define a handler for a given code, Flask falls back to the app-level handler. This means you can define a single JSON error handler in each API blueprint and keep the app-level handlers for HTML responses.
JSON API Error Responses
Content Negotiation
A well-designed API should return JSON errors when the client sends Accept: application/json and HTML errors otherwise:
from flask import request, jsonify, render_template
from werkzeug.exceptions import HTTPException
@app.errorhandler(HTTPException)
def handle_http_exception(error: HTTPException):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
return jsonify({
'error': error.description,
'status': error.code,
}), error.code
return render_template(f'errors/{error.code}.html', error=error), error.code
RFC 7807 Problem Details Format
For public APIs, consider the RFC 7807 Problem Details format:
from flask import jsonify
from werkzeug.exceptions import HTTPException
def problem_response(status: int, title: str, detail: str,
type_url: str = 'about:blank') -> tuple:
body = {
'type': type_url,
'title': title,
'status': status,
'detail': detail,
}
response = jsonify(body)
response.content_type = 'application/problem+json'
return response, status
@app.errorhandler(404)
def not_found(error):
return problem_response(
404, 'Not Found',
'The requested resource does not exist.',
'https://protocolcodes.com/http/404-not-found/',
)
Werkzeug Exception Hierarchy
Werkzeug's exception tree lets you write class-based error handlers that catch entire families of errors:
HTTPException
├── BadRequest (400)
│ ├── BadRequestKeyError
│ └── RequestEntityTooLarge (413)
├── Unauthorized (401)
├── Forbidden (403)
├── NotFound (404)
├── MethodNotAllowed (405)
├── Conflict (409)
├── Gone (410)
├── UnprocessableEntity (422)
├── TooManyRequests (429)
└── InternalServerError (500)
You can catch all client errors with one handler:
from werkzeug.exceptions import BadRequest
@app.errorhandler(BadRequest)
def handle_bad_request(error: BadRequest):
return jsonify({'error': error.description}), error.code
Custom exceptions can extend these classes to inherit the correct status code:
from werkzeug.exceptions import UnprocessableEntity
class ValidationError(UnprocessableEntity):
def __init__(self, fields: dict[str, list[str]]):
super().__init__()
self.fields = fields
@app.errorhandler(ValidationError)
def handle_validation_error(error: ValidationError):
return jsonify({
'error': 'Validation failed',
'fields': error.fields,
}), 422
Production Setup
Logging Integration
Flask's app.logger is a standard Python logging.Logger. Configure it to log all unhandled exceptions with full stack traces:
import logging
from flask import Flask
app = Flask(__name__)
@app.errorhandler(Exception)
def handle_unexpected(error: Exception):
app.logger.exception('Unhandled exception: %s', str(error))
return jsonify({'error': 'Internal server error'}), 500
Sentry Integration
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
sentry_sdk.init(
dsn='https://[email protected]/project',
integrations=[FlaskIntegration()],
traces_sample_rate=0.1,
)
The Flask integration automatically captures unhandled exceptions and reports them to Sentry. It also adds request context (URL, method, headers) to each event.
Debug Mode Pitfalls
In debug mode (FLASK_DEBUG=1), Flask shows its interactive debugger in the browser instead of calling your error handlers. This is a security risk if accidentally enabled in production — set DEBUG=False explicitly in production config and use FLASK_ENV=production.
Key Takeaways
- Register
@app.errorhandler(HTTPException)to catch all HTTP errors in one place - Use blueprint-scoped handlers to ensure API routes always return JSON errors
- Extend Werkzeug exception classes for custom errors that carry the correct status code
- The RFC 7807 Problem Details format (
application/problem+json) is the standard for API errors - Never run Flask with
DEBUG=Truein production — it exposes an interactive debugger