Flask Error Handling: Custom Exceptions and Logging

In web development, error handling is a critical aspect to ensure application stability and a good user experience. When users access non-existent pages or the server encounters unexpected situations, reasonable error handling not only provides users with friendly prompts but also helps developers quickly identify issues. The Flask framework offers a flexible error handling mechanism, allowing us to easily implement custom exceptions and logging.

Why Error Handling is Necessary?

Imagine if a user accesses a non-existent page and sees a stack of cryptic code errors (e.g., “500 Internal Server Error”). This is clearly unfriendly. Error handling can:
- Return clear and understandable prompts to users (e.g., “Page not found”)
- Record detailed information about error occurrences for development and debugging
- Prevent the program from crashing due to unhandled exceptions

Flask’s Default Error Handling

Flask has built-in handling for common errors like 404 (page not found) and 500 (internal server error). We can customize the response content for these errors using the @app.errorhandler decorator.

Example: Handling 404 and 500 Errors

from flask import Flask

app = Flask(__name__)

# Handle 404 errors (page not found)
@app.errorhandler(404)
def page_not_found(error):
    return "Oops, the page you're looking for has disappeared~ 404 Not Found", 404  # Return a friendly message and status code

# Handle 500 errors (internal server error)
@app.errorhandler(500)
def server_error(error):
    return "The server is taking a coffee break. Please try again later~ 500 Internal Server Error", 500

# Test route
@app.route('/')
def home():
    return "Welcome to the homepage!"

if __name__ == '__main__':
    app.run(debug=True)  # Shows detailed errors when debug=True, but disable in production

Key Points:
- @app.errorhandler(code_or_exception): Specify the error type to handle (can be a status code or exception class)
- The return value can be a string, template, or JSON. It’s recommended to include a status code (e.g., 404)
- By default, Flask shows the error stack when debug=True, but use custom pages in production

Custom Exceptions: Making Errors “Smarter”

When specific errors occur in program logic (e.g., “user not found”), returning status codes directly may not be clear enough. We can use custom exception classes to encapsulate error information, making the handling logic more modular.

Step 1: Define a Custom Exception Class

class UserNotFoundError(Exception):
    """Custom exception for user not found"""
    def __init__(self, user_id):
        self.user_id = user_id  # Store error-related information (e.g., user ID)
        self.message = f"User ID {user_id} does not exist and cannot be queried"
        super().__init__(self.message)  # Call parent class constructor

Step 2: Raise the Exception Actively

In business logic, proactively raise custom exceptions when error scenarios occur:

@app.route('/user/<int:user_id>')
def get_user(user_id):
    # Simulate database query (assuming only users with IDs 1 and 2 exist)
    valid_users = {1, 2}
    if user_id not in valid_users:
        raise UserNotFoundError(user_id)  # Raise custom exception
    return f"User {user_id} information: Name: Zhang San"

Step 3: Catch and Handle Custom Exceptions

Use @app.errorhandler to catch custom exceptions and return friendly prompts:

@app.errorhandler(UserNotFoundError)
def handle_user_error(error):
    # Return error message with 404 status code
    return error.message, 404

Effect: When a user accesses /user/3 (non-existent user ID), the response will be “User ID 3 does not exist and cannot be queried”.

Logging: Making Errors “Traceable”

Logging is a key tool for troubleshooting. Flask provides a logging system based on Python’s logging module, which can record error information to the console or file for subsequent analysis.

Flask Logging Basics

  • Log Levels (from lowest to highest): DEBUG, INFO, WARNING, ERROR, CRITICAL
  • Default Configuration: Flask outputs logs to the console by default. In production, manually configure file output.

Configuring Logs to File

import logging
from logging.handlers import RotatingFileHandler

# Create Flask app
app = Flask(__name__)

# Configure logging: write to file with size and backup limits
file_handler = RotatingFileHandler(
    'app.log',          # Log file name
    maxBytes=1024*1024, # Max size per log file: 1MB
    backupCount=10      # Max 10 backup files
)
# Set log format (time, module, level, message)
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
file_handler.setFormatter(formatter)
# Add log handler to Flask's logger
app.logger.addHandler(file_handler)
# Set log level to INFO (only record INFO and above)
app.logger.setLevel(logging.INFO)

Logging Different Types of Events

@app.route('/user/<int:user_id>')
def get_user(user_id):
    valid_users = {1, 2}
    if user_id not in valid_users:
        # Log an error (ERROR level)
        app.logger.error(f"User query failed: User ID {user_id} does not exist")
        raise UserNotFoundError(user_id)
    # Log normal operation (INFO level)
    app.logger.info(f"User {user_id} query successful")
    return f"User {user_id} information: Name: Zhang San"

Effect: Error logs will be written to both the console and app.log file, with content similar to:

2023-10-01 12:30:00,123 - flask.app - ERROR - User query failed: User ID 3 does not exist

Comprehensive Example: Complete Error Handling for User Queries

Combining custom exceptions and logging, here’s a complete user query implementation:

from flask import Flask
import logging
from logging.handlers import RotatingFileHandler

# 1. Define custom exception
class UserNotFoundError(Exception):
    def __init__(self, user_id):
        self.user_id = user_id
        self.message = f"User ID {user_id} does not exist"
        super().__init__(self.message)

# 2. Configure logging
app = Flask(__name__)
file_handler = RotatingFileHandler('app.log', maxBytes=1024*1024, backupCount=10)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)

# 3. Error handling function
@app.errorhandler(UserNotFoundError)
def handle_user_error(error):
    app.logger.error(f"User not found error caught: {error.message}")
    return error.message, 404

# 4. Business route
@app.route('/user/<int:user_id>')
def get_user(user_id):
    valid_users = {1, 2}  # Assume these user IDs exist in the database
    if user_id not in valid_users:
        app.logger.error(f"Querying non-existent user: {user_id}")
        raise UserNotFoundError(user_id)
    app.logger.info(f"User {user_id} query successful")
    return f"User information: ID={user_id}, Name=Zhang San"

# 5. Test 404 error
@app.errorhandler(404)
def page_not_found(error):
    app.logger.warning("Accessing a non-existent page")
    return "Page lost~ Please check if the URL is correct", 404

if __name__ == '__main__':
    app.run(debug=False)  # Disable debug mode in production

Summary

With custom exceptions and logging, Flask helps us:
- Provide users with friendlier error prompts (avoid crashes)
- Precisely locate issues (detailed error information in logs)
- Make error handling logic clearer (modular design)

Key Tips:
- Use @app.errorhandler to handle default errors like 404/500
- Custom exception classes to encapsulate business errors (e.g., user not found)
- Configure logs to files to avoid relying on console output
- Differentiate importance with log levels (ERROR for critical errors)

This article should help you quickly master the core techniques of Flask error handling, making your application more robust and maintainable!

Xiaoye