Framework Cookbooks

Flask Error Handling: Custom Pages, API Errors, and Blueprints

Comprehensive error handling in Flask — @app.errorhandler, blueprint-scoped handlers, JSON error responses for APIs, and Werkzeug exception hierarchy.

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=True in production — it exposes an interactive debugger

Related Protocols

Related Glossary Terms

More in Framework Cookbooks