Error Handling and Logging

Introduction

Users hit wrong URLs. Code throws exceptions. Production must return friendly error pages without exposing stack traces, while developers need logs to diagnose issues. This chapter covers @app.errorhandler, debug vs production behavior, and configuring Python logging for Flask apps.

Prerequisites

HTTP Error Handlers

templates/errors/404.html:

html
{% extends "base.html" %}
{% block content %}
  <h1>404 — Page not found</h1>
  <p>The page you requested does not exist.</p>
  <a href="{{ url_for('index') }}">Go home</a>
{% endblock %}

Code explanation:

  • errorhandler registers a view for a status code or exception class
  • Return status tuple so the HTTP code matches the error

abort in Views

python
from flask import Flask, abort
 
app = Flask(__name__)
 
@app.route("/posts/<int:post_id>")
def show_post(post_id):
    post = None
    if post is None:
        abort(404)
    return render_template("post.html", post=post)

Custom message:

python
abort(403, description="You cannot edit this post.")

Read in handler:

python
@app.errorhandler(403)
def forbidden(e):
    return render_template("errors/403.html", message=e.description), 403

Debug Mode vs Production

Development (DEBUG=True):

  • Interactive Werkzeug debugger in browser on 500 errors
  • Auto reload on code changes

Warning

Never Enable Debug in Production

The debugger allows arbitrary Python execution on the server—critical security risk if exposed publicly.

Production:

python
app.config["DEBUG"] = False
app.config["PROPAGATE_EXCEPTIONS"] = False

Use Gunicorn with debug off—see deployment chapter.

Flask app.logger

Levels: debug, info, warning, error, exception (includes traceback).

Configure Logging

Code explanation:

  • RotatingFileHandler prevents log files from filling the disk
  • Gunicorn also logs to stdout—collect with systemd or Docker

Request Context in Logs

Structured logging (JSON lines) helps log aggregators—concept for larger teams.

Handle Specific Exceptions

python
class AppError(Exception):
    def __init__(self, message, status_code=400):
        super().__init__(message)
        self.message = message
        self.status_code = status_code
 
@app.errorhandler(AppError)
def handle_app_error(e):
    return jsonify({"error": e.message}), e.status_code

Raise in business logic:

python
raise AppError("Email already registered", 409)

FAQ

Custom 404 still shows Werkzeug page?

Ensure handler is registered before app.run and DEBUG template exists.

Log duplicate lines?

Flask/Werkzeug may add handlers—check app.logger.handlers; avoid duplicate basicConfig.

500 page in dev shows debugger?

Expected with DEBUG=True—disable debug to test custom 500 template.

Log sensitive data?

Never log passwords, tokens, or full credit card numbers.

Gunicorn logs vs app logs?

Gunicorn logs worker crashes; app.logger logs application events—use both.

JSON API always return JSON errors?

Branch on request.path.startswith("/api/") or request.accept_mimes.accept_json.