Skip to main content

Overview

Flask provides a flexible error handling system that allows you to customize how your application responds to errors, from 404 pages to unhandled exceptions.

Error Handler Basics

Handling HTTP Errors

from flask import Flask, render_template

app = Flask(__name__)

@app.errorhandler(404)
def page_not_found(error):
    """Handle 404 errors."""
    return render_template('errors/404.html'), 404

@app.errorhandler(403)
def forbidden(error):
    """Handle 403 errors."""
    return render_template('errors/403.html'), 403

@app.errorhandler(500)
def internal_server_error(error):
    """Handle 500 errors."""
    return render_template('errors/500.html'), 500

Handling Custom Exceptions

class InvalidAPIUsage(Exception):
    """Custom exception for API errors."""
    status_code = 400

    def __init__(self, message, status_code=None, payload=None):
        super().__init__()
        self.message = message
        if status_code is not None:
            self.status_code = status_code
        self.payload = payload

    def to_dict(self):
        rv = dict(self.payload or ())
        rv['message'] = self.message
        return rv

@app.errorhandler(InvalidAPIUsage)
def handle_invalid_api_usage(error):
    """Handle custom API errors."""
    response = jsonify(error.to_dict())
    response.status_code = error.status_code
    return response

# Use the custom exception
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
    user = User.query.get(user_id)
    if user is None:
        raise InvalidAPIUsage('User not found', status_code=404)
    return jsonify(user.to_dict())

Error Handler Hierarchy

Handling Exception Classes

class DatabaseError(Exception):
    pass

class ConnectionError(DatabaseError):
    pass

class QueryError(DatabaseError):
    pass

# Catches all DatabaseError subclasses
@app.errorhandler(DatabaseError)
def handle_database_error(error):
    """Handle all database-related errors."""
    app.logger.error(f'Database error: {error}')
    return 'Database error occurred', 500

# More specific handler takes precedence
@app.errorhandler(ConnectionError)
def handle_connection_error(error):
    """Handle connection errors specifically."""
    return 'Could not connect to database', 503

Handling HTTP Exceptions

from werkzeug.exceptions import HTTPException

# Catch all HTTP exceptions
@app.errorhandler(HTTPException)
def handle_http_exception(error):
    """Handle all HTTP exceptions."""
    return render_template(
        'errors/generic.html',
        code=error.code,
        name=error.name,
        description=error.description
    ), error.code

Blueprint Error Handlers

Blueprint-Specific Handlers

from flask import Blueprint

api = Blueprint('api', __name__, url_prefix='/api')

@api.errorhandler(404)
def api_not_found(error):
    """Handle 404 errors for API routes only."""
    return jsonify({'error': 'Resource not found'}), 404

@api.errorhandler(ValueError)
def api_value_error(error):
    """Handle ValueError in API routes only."""
    return jsonify({'error': str(error)}), 400

App-Wide Handlers from Blueprints

@api.app_errorhandler(404)
def global_not_found(error):
    """Handle 404 errors for all routes, not just API."""
    if request.path.startswith('/api/'):
        return jsonify({'error': 'Not found'}), 404
    return render_template('errors/404.html'), 404

Registering Error Handlers

Using register_error_handler()

def handle_validation_error(error):
    return jsonify({'error': 'Validation failed'}), 400

# Register without decorator
app.register_error_handler(ValidationError, handle_validation_error)
app.register_error_handler(400, handle_validation_error)

Conditional Registration

if app.config['USE_CUSTOM_ERROR_PAGES']:
    app.register_error_handler(404, custom_404_handler)
else:
    app.register_error_handler(404, default_404_handler)

Error Handling Patterns

JSON API Errors

from werkzeug.exceptions import HTTPException

@app.errorhandler(HTTPException)
def handle_api_error(error):
    """Return JSON for API errors."""
    response = {
        'error': {
            'code': error.code,
            'name': error.name,
            'description': error.description,
        }
    }
    return jsonify(response), error.code

Content Negotiation

@app.errorhandler(404)
def not_found(error):
    """Return JSON or HTML based on request."""
    if request.accept_mimetypes.accept_json and \
       not request.accept_mimetypes.accept_html:
        return jsonify({'error': 'Not found'}), 404
    return render_template('errors/404.html'), 404

Logging Errors

import traceback

@app.errorhandler(500)
def internal_error(error):
    """Log and handle internal server errors."""
    app.logger.error('Server Error: %s', error)
    app.logger.error(traceback.format_exc())
    
    # Send to error tracking service
    if app.config.get('SENTRY_DSN'):
        sentry_sdk.capture_exception(error)
    
    return render_template('errors/500.html'), 500

Database Rollback

from flask import g

@app.errorhandler(Exception)
def handle_exception(error):
    """Rollback database on errors."""
    if hasattr(g, 'db'):
        g.db.session.rollback()
    
    # Re-raise if in debug mode
    if app.debug:
        raise error
    
    app.logger.exception('Unhandled exception')
    return 'An error occurred', 500

Error Context

Accessing Request Context

from flask import request

@app.errorhandler(500)
def server_error(error):
    """Include request details in error response."""
    app.logger.error(
        f'Server error on {request.method} {request.path}\n'
        f'User-Agent: {request.user_agent}\n'
        f'Remote addr: {request.remote_addr}\n'
        f'Error: {error}'
    )
    return render_template('errors/500.html'), 500

Error Details in Templates

@app.errorhandler(404)
def not_found(error):
    """Pass error details to template."""
    return render_template(
        'errors/404.html',
        path=request.path,
        method=request.method,
        referrer=request.referrer
    ), 404
<!-- templates/errors/404.html -->
<h1>Page Not Found</h1>
<p>The requested URL <code>{{ path }}</code> was not found.</p>
{% if referrer %}
<p><a href="{{ referrer }}">Go back</a></p>
{% endif %}

Aborting Requests

Using abort()

from flask import abort

@app.route('/admin')
def admin():
    if not current_user.is_admin:
        abort(403)
    return render_template('admin.html')

@app.route('/users/<int:user_id>')
def show_user(user_id):
    user = User.query.get(user_id)
    if user is None:
        abort(404)
    return render_template('user.html', user=user)

Custom Abort Messages

from werkzeug.exceptions import NotFound

@app.route('/posts/<int:post_id>')
def show_post(post_id):
    post = Post.query.get(post_id)
    if post is None:
        abort(404, description="Post not found")
    return render_template('post.html', post=post)

@app.errorhandler(404)
def not_found(error):
    return render_template(
        'errors/404.html',
        message=error.description
    ), 404

Exception Handling Methods

Flask provides several methods for handling exceptions:

handle_http_exception()

# Called for HTTPException instances
# Default behavior: looks for registered error handler
# Falls back to returning the exception response

handle_user_exception()

# Called for exceptions during request handling
# Processes HTTPException and looks for error handlers
# Re-raises if no handler found

handle_exception()

# Called for unhandled exceptions
# Always creates a 500 InternalServerError
# Sends got_request_exception signal

Error Handling Configuration

PROPAGATE_EXCEPTIONS

# Re-raise exceptions instead of handling them
app.config['PROPAGATE_EXCEPTIONS'] = True

TRAP_HTTP_EXCEPTIONS

# Don't handle HTTPException, let debugger catch them
app.config['TRAP_HTTP_EXCEPTIONS'] = True

TRAP_BAD_REQUEST_ERRORS

# Show detailed errors for bad request key errors
app.config['TRAP_BAD_REQUEST_ERRORS'] = True

Best Practices

1. Use Appropriate Status Codes

# 400 - Bad Request (client error)
if not request.json:
    abort(400, description="Request must be JSON")

# 401 - Unauthorized (authentication required)
if not current_user.is_authenticated:
    abort(401)

# 403 - Forbidden (authenticated but not allowed)
if not current_user.can_edit(resource):
    abort(403)

# 404 - Not Found
if resource is None:
    abort(404)

# 422 - Unprocessable Entity (validation error)
if not form.validate():
    abort(422)

# 500 - Internal Server Error (server problem)
if database_connection_failed:
    abort(500)

2. Provide Helpful Error Messages

@app.errorhandler(404)
def not_found(error):
    """Helpful 404 page with suggestions."""
    # Log the 404
    app.logger.warning(f'404 error: {request.path}')
    
    # Suggest similar paths
    similar = find_similar_paths(request.path)
    
    return render_template(
        'errors/404.html',
        similar_paths=similar
    ), 404

3. Don’t Leak Sensitive Information

@app.errorhandler(500)
def server_error(error):
    """Never show stack traces in production."""
    if app.debug:
        # Detailed error in development
        raise error
    
    # Generic message in production
    return render_template('errors/500.html'), 500

4. Log All Errors

import logging

@app.errorhandler(Exception)
def log_exception(error):
    """Log all unhandled exceptions."""
    app.logger.error(
        'Unhandled Exception',
        exc_info=error,
        extra={
            'url': request.url,
            'method': request.method,
            'ip': request.remote_addr,
            'user_agent': str(request.user_agent)
        }
    )
    return 'Internal Server Error', 500

5. Test Error Handlers

def test_404_handler(client):
    """Test 404 error handler."""
    response = client.get('/nonexistent')
    assert response.status_code == 404
    assert b'Not Found' in response.data

def test_custom_exception(client):
    """Test custom exception handler."""
    response = client.get('/api/invalid')
    assert response.status_code == 400
    data = response.get_json()
    assert 'error' in data

Debugging Errors

Debug Mode

app.config['DEBUG'] = True

# Or via environment
export FLASK_DEBUG=1
flask run

Interactive Debugger

# Werkzeug debugger with interactive console
app.config['DEBUG'] = True
app.run(use_debugger=True)

Custom Error Pages in Debug

if app.debug:
    @app.errorhandler(404)
    def debug_404(error):
        return render_template(
            'errors/debug_404.html',
            routes=app.url_map
        ), 404

Build docs developers (and LLMs) love