Back to Scaling Python Applications guides

Flask Error Handling Patterns

Stanley Ulili
Updated on February 25, 2025

Applications fail, servers fail—sooner or later, your Flask app will encounter an error in production. Even if your code is flawless, external factors like network issues, overloaded databases, or hardware failures can cause unexpected crashes.

This article covers Flask error handling patterns to help you handle exceptions gracefully.

Let's get started!

What are errors in Flask?

Errors in Flask occur when something unexpected happens during the execution of a request. They can be categorized into three main types:

Operational errors

These errors occur due to external conditions rather than issues in the application code. They are expected and should be handled gracefully to maintain application stability.

Common examples include:
- A client requests a non-existent resource (404 Not Found).
- Invalid input results in a validation failure (400 Bad Request).
- A database connection failure due to network issues (500 Internal Server Error).
- External API request failures or timeouts.

Programming errors

These errors stem from mistakes in the code and usually require debugging rather than runtime handling.

Examples include:
- Calling a function that does not exist, leading to a NameError.
- Passing incorrect arguments to a function, triggering a TypeError.
- Accessing an undefined variable, causing unexpected behavior.
- Failing to handle exceptions within asynchronous tasks.

System errors

These errors occur at the operating system or hardware level and can potentially crash a Flask app if not handled correctly.

Common system errors include:
- A file system error preventing file read/write operations.
- Database connection failures due to server unavailability.
- Memory leaks or excessive resource consumption leading to performance degradation.

Now that we understand different types of errors in Flask let’s explore the best practices for handling them effectively.

Using error handlers in Flask

Handling errors properly in a Flask app is essential for providing clients with a predictable and structured response. Returning raw error messages or HTML responses can be unhelpful when a request fails due to a missing resource, invalid input, or an internal server issue.

Instead, Flask allows you to define custom error handlers that generate structured JSON responses, ensuring that clients receive consistent and meaningful feedback.

A custom error handler can be registered using the @app.errorhandler decorator. This function receives an instance of the raised error and returns a response with the appropriate status code.

 
from flask import Flask, jsonify
import werkzeug.exceptions

app = Flask(__name__)

@app.errorhandler(werkzeug.exceptions.NotFound)
def handle_404_error(e):
    response = {
        "error": "Not Found",
        "message": "The requested resource was not found.",
        "status": 404
    }
    return jsonify(response), 404

When a client requests an invalid endpoint, this handler triggers automatically and returns a structured JSON response instead of a default HTML page.

Malformed requests that fail validation should return a 400 Bad Request error with a clear explanation of what went wrong.

Flask allows error handlers to capture these issues and provide meaningful responses to API consumers.

 
@app.errorhandler(werkzeug.exceptions.BadRequest)
def handle_400_error(e):
    response = {
        "error": "Bad Request",
        "message": "The request could not be understood or was missing required parameters.",
        "status": 400
    }
    return jsonify(response), 400

Defining this handler ensures clients receive structured feedback when sending an invalid request.

Creating custom exceptions

Some errors may not fit within standard HTTP error codes, requiring custom exceptions for more precise error handling. Flask lets you define your own exception classes that extend werkzeug.exceptions.HTTPException.

 
from werkzeug.exceptions import HTTPException

class InvalidUsage(HTTPException):
    code = 422
    description = "Invalid input or processing error."

@app.errorhandler(InvalidUsage)
def handle_invalid_usage(e):
    response = {
        "error": "Invalid Usage",
        "message": e.description,
        "status": e.code
    }
    return jsonify(response), e.code

Once defined, this custom exception can be raised anywhere in the API to return a structured error response.

 
@app.route("/process")
def process_data():
    raise InvalidUsage("The provided data format is incorrect.")

When a client sends a request that triggers this exception, the API responds with a predefined message and a 422 status code.

Catching unexpected errors

In complex applications, some errors may not be explicitly handled. Instead of returning raw Python tracebacks to the client, a general exception handler can be defined to provide a standardized response for unexpected failures.

 
@app.errorhandler(Exception)
def handle_generic_exception(e):
    response = {
        "error": "Unexpected Error",
        "message": "An unknown error occurred. Please contact support.",
        "status": 500
    }
    return jsonify(response), 500

This ensures that even unhandled errors result in a structured response rather than an unpredictable failure message.

Custom error pages with abort()

Sometimes, an app must stop processing a request and immediately return an error response. Flask provides the abort() function, which raises an HTTPException and stops request execution. This is useful when a request fails validation, a required resource is missing, or an action is not allowed.

Flask’s default behavior when using abort() is to return a plain-text response with the associated HTTP status code. This may be enough for debugging, but returning structured JSON responses for production APIs provides a more consistent and user-friendly experience.

When an API requires a specific query parameter, abort(400) can be used to return a 400 Bad Request error when the parameter is missing.

 
from flask import Flask, jsonify, request, abort

app = Flask(__name__)

@app.route("/profile")
def user_profile():
    username = request.args.get("username")

    if username is None:
        abort(400)

    user = get_user(username=username)

    if user is None:
        abort(404)

    return jsonify({"username": user.username, "email": user.email})

In this example, if a client does not provide a username parameter in the query string, the request is aborted with a 400 Bad Request error. If the username does not exist in the system, the request is aborted with a 404 Not Found error.

Flask’s default error pages for abort() calls are plain text, which may not be suitable for an API that expects structured responses. To improve this, error handlers can be combined with abort() to ensure API clients receive a JSON response.

 
@app.errorhandler(400)
def handle_400_error(e):
    response = {
        "error": "Bad Request",
        "message": "A username parameter is required.",
        "status": 400
    }
    return jsonify(response), 400

@app.errorhandler(404)
def handle_404_error(e):
    response = {
        "error": "Not Found",
        "message": "The requested user does not exist.",
        "status": 404
    }
    return jsonify(response), 404

Now, when abort(400) or abort(404) is triggered, Flask automatically returns the custom JSON response instead of the default plain-text error page.

For cases where a request violates business logic, abort() can be used alongside custom exceptions to return more descriptive error responses.

 
@app.route("/delete-account")
def delete_account():
    if not user_has_permission():
        abort(403)

    return jsonify({"message": "Account deleted successfully."})

In this example, if the user lacks permission to delete an account, the request is aborted with a 403 Forbidden response.

Using abort() in combination with custom error handlers ensures that API errors are handled gracefully while maintaining a structured response format. Instead of exposing default error messages, APIs can provide meaningful feedback, improving the client experience and making error handling more predictable.

Handling errors in asynchronous code

Flask supports asynchronous views with the async and await syntax when installed with the async extra (pip install flask[async]).

This powerful capability allows views to perform concurrent operations like multiple database queries or external API calls. However, asynchronous functions require special error handling approaches to prevent unexpected failures.

Asynchronous errors can bypass standard error handlers if not correctly managed, causing application crashes or silent failures that are difficult to debug.

When defining asynchronous route handlers in Flask, you must explicitly catch exceptions that might occur during await operations:

 
@app.route("/user/<id>")
async def get_user(id):
    try:
        user = await db.fetch_user(id)
        if not user:
            abort(404)
        return jsonify(user)
    except DatabaseConnectionError:
        abort(503)
    except Exception:
        abort(500)

In this example, the async route handler properly catches exceptions from the database query. It converts them to appropriate HTTP status codes using Flask's abort() function, which triggers the registered error handlers.

Flask's async support has important limitations regarding background tasks. Tasks spawned in an async view that don't complete before the view returns will be cancelled automatically:

 
@app.route("/process", methods=["POST"])
async def process_data():
    data = request.get_json()

    # DON'T DO THIS - task will be cancelled when view returns
    asyncio.create_task(background_process(data))

    return jsonify({"status": "processing"})

For true background processing, consider using a task queue like Celery or RQ instead of trying to spawn background tasks directly from Flask views.

Be aware that older Flask extensions may not support async views properly. If they provide decorators to enhance views, these might not work correctly with async functions:

 
# This might not work correctly with an async view
@some_extension.decorator
async def async_view():
    result = await async_operation()
    return jsonify(result)

Check the extension documentation to verify async support or use Flask's ensure_sync() method when needed.

Remember that async isn't inherently faster—it's best used for managing concurrent IO-bound operations where traditional synchronous views would be blocked waiting for responses.

Logging errors in Flask

Logging errors is essential for monitoring and debugging Flask applications, especially in production environments where debugging information is not visible to users.

Flask uses Python’s built-in logging module, allowing you to capture and store error messages efficiently. Logging helps track application failures, analyze patterns, and proactively address potential issues before they impact users.

Flask provides a built-in logger, app.logger, which can record errors, warnings, and informational messages. This logger automatically inherits the name of the Flask application and is configured to log messages to the WSGI server’s error stream by default.

A basic example of logging within a Flask route:

 
@app.route('/login', methods=['POST'])
def login():
    user = get_user(request.form['username'])

    if user and user.check_password(request.form['password']):
        login_user(user)
        app.logger.info('%s logged in successfully', user.username)
        return redirect(url_for('index'))
    else:
        app.logger.warning('%s failed to log in', user.username)
        abort(401)

In this example, successful login attempts are logged at the INFO level, while failed attempts are logged at the WARNING level. This makes it easier to track authentication activity and detect suspicious login failures.

Configuring logging in Flask

By default, Flask sets up a basic logger, but for better visibility and control, logging should be configured explicitly at the start of the application. Using dictConfig(), Flask’s logging can be customized to format messages and define logging levels.

 
from logging.config import dictConfig

dictConfig({
    'version': 1,
    'formatters': {'default': {
        'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
    }},
    'handlers': {'wsgi': {
        'class': 'logging.StreamHandler',
        'stream': 'ext://flask.logging.wsgi_errors_stream',
        'formatter': 'default'
    }},
    'root': {
        'level': 'INFO',
        'handlers': ['wsgi']
    }
})

app = Flask(__name__)

This configuration ensures that logs include timestamps, log levels, and the module generating the log message. The logging level is set to INFO, meaning only messages with INFO, WARNING, ERROR, or CRITICAL severity will be recorded.

If the application’s logger is accessed before custom logging is configured, Flask will automatically attach a default logging handler. This handler can be removed if a different logging setup is preferred.

 
from flask.logging import default_handler

app.logger.removeHandler(default_handler)

Removing the default handler prevents redundant log messages when using a custom logging system.

Centralizing logs with Better Stack

Logging errors locally is useful for debugging, but a centralized logging solution is essential for tracking issues in production. Better Stack provides a real-time logging platform that aggregates, filters, and visualizes logs, helping you detect and resolve issues faster.

To send logs from Flask to Better Stack, install the logtail-python package, which provides the necessary handler for forwarding logs.

 
pip install logtail-python

Once installed, configure Flask’s logging system to use LogtailHandler, ensuring all logs are sent to Better Stack:

 
from flask import Flask
from logging.config import dictConfig
from logtail import LogtailHandler

dictConfig({
    "version": 1,
    "formatters": {
        "default": {
            "format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s"
        },
    },
    "handlers": {
        "logtail": {
            "class": "logtail.LogtailHandler",
            "source_token": "YOUR_BETTER_STACK_SOURCE_TOKEN",
            "formatter": "default",
        },
    },
    "root": {
        "level": "INFO",
        "handlers": ["logtail"]
    },
})

app = Flask(__name__)

app.logger.info("Flask logs are now being sent to Better Stack.")

This configuration ensures all logs are correctly formatted and sent to Better Stack in real time.

Once Flask is configured, logs will appear in the Better Stack dashboard's Live Tail section. This interface allows developers to filter logs by severity, timestamp, or specific error messages.

Better Stack Logs Dashboard

Using structured logs with JSON-based attributes improves filtering and searchability, making debugging specific API errors easier or tracking down performance bottlenecks.

Final thoughts

This article covered key strategies, including identifying error types, creating custom error handlers, handling asynchronous errors, and setting up structured logging with Better Stack.

Implementing these patterns improves resilience, enhances user experience, and simplifies debugging. For more, refer to Flask’s official documentation on error handling and debugging.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
A Comprehensive Guide to Logging in Python
Python provides a built-in logging module in its standard library that provides comprehensive logging capabilities for Python programs
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github