Handle The Errors with Flask Correctly

You won’t need to handle the errors in a Flask application if you predict all the situations. But let’s face it, the user won’t use the application correctly, or you’ll forget to handle the case when a user purchase an item during the summer time change.

So, just in case of, let’s see how to handle errors in a Flask application and what are the best practices.

Why Bother Managing Errors?

Picture this: a user faces an error page and sees something like AttributeError: 'NoneType' object has no attribute 'something'. Not only do they have no idea what just happened, but they’re also wondering why they trusted your app in the first place. Worse, they might have just seen internal details of your application: a hacker’s dream.

Managing errors isn’t just about aesthetics; it’s about:

  • Security: Don’t expose sensitive details about your app.
  • Usability: Provide helpful, user-friendly error messages.
  • Debugging: Organize errors for easier troubleshooting.

In short, manage your errors, or they’ll manage you.

Custom Exception

First things first: centralize your errors by creating a custom exception. This exception will act as a wrapper for all application-specific issues, keeping things neat and giving you a single point of control.

class AppException(Exception):
    def __init__(self, message, http_code):
        super(Exception, self).__init__(message)
        self.message = message
        self.http_code = http_code

Now you can use AppException to represent any error in your application with a custom message and an appropriate HTTP status code. This not only keeps your code consistent but also prevents you from scattering raise statements like confetti.

Blueprint

Blueprints aren’t just for organizing routes, they’re also a great way to manage errors.

Let’s set up a blueprint that catches all your custom errors and ensures they’re returned in a structured format.

error_bp = Blueprint("errors", __name__)
LOGGER = logging.getLogger(__name__)

@error_bp.app_errorhandler(AppException)
def handle_custom_exception(error):
    LOGGER.warning(f"Error occurred for request {request.path} : {error.message}")
    response = {"error": error.message}
    return make_response(jsonify(response), error.http_code)

Register the blueprint in your app, and you’re good to go:

app.register_blueprint(errors)

Now, any AppException raised in your app will be handled by this blueprint.

Not Found Errors

Sometimes, users (or bots) will hit non-existent routes, triggering Flask’s dreaded 404 Not Found. First, I want to know who requests non-existent routes: an outdated system which still uses my application, or some guy which tries some random routes.

So, I log all the Not Found error and add a personal touch by logging the source IP.

@error_bp.app_errorhandler(NotFound)
def handle_not_found(error):
    source_ip = request.remote_addr
    message = f"Cannot find the following url requested by {source_ip}: {request.path}"
    LOGGER.warning(message)

    return make_response(jsonify({"error": message}), 404)

This logs who’s knocking on your door and why, making it easier to identify (and fix) recurring issues from specific sources.

SQLAlchemy Errors

Database errors are among the most common issues you’ll face. Instead of letting SQLAlchemy throw verbose and cryptic errors, let’s intercept them and log useful details, like the table and fields involved.

@error_bp.app_errorhandler(SQLAlchemyError)
def other(error):
    LOGGER.exception(error)
    platform = os.getenv("PLATFORM", "local")

    if platform != "prod":
        return make_response(jsonify({"error": str(error)}), 500)
    return make_response(
        jsonify({"error": "Unknown error. Please contact the support service."}),
        500,
    )

By logging the error details, you’ll have the context needed to debug issues without sharing those juicy SQL errors with end users.

But to hide most of the sensitive information, I first check the platform where is running the application. If it’s running in production, I return a static message like "Unknown error". Otherwise, if I’m in a development or a staging environment, I can return the full description of the error.

Remaining Errors

For everything else that slips through the cracks, set up a generic error handler. If a specific message is available, log it; otherwise, keep it vague (especially in production).

Example:

@error_bp.app_errorhandler(Exception)
def handle_generic_exception(error):
    LOGGER.exception(f"Error while handling {request.method} {request.full_path} ({request.data}): {error}")
    platform = os.getenv("PLATFORM", "local")

    http_code = 500
    if hasattr(error, "code"):
        http_code = error.code

    message = str(error)

    if platform != "prod":
        return make_response(jsonify({"error": message}), http_code)
    return make_response(
        jsonify({"error": "Unknown error. Please contact the support service."}),
        http_code,
    )

This catch-all handler ensures you never expose raw errors to users, even if something unexpected occurs.

Conclusion

Error management isn’t just about writing code; it’s about writing the right code for the environment. In production, less is more. Avoid logging sensitive details or displaying raw error messages, hackers don’t need your help.

In summary:

  • Use custom exceptions to centralize error handling.
  • Set up blueprints to handle errors cleanly.
  • Log useful details for debugging, but only in development.
  • Remember: no one loves a stack trace on their screen.

Never Miss Another Tech Innovation

Concrete insights and actionable resources delivered straight to your inbox to boost your developer career.

My New ebook, Best Practices To Create A Backend With Spring Boot 3, is available now.

Best practices to create a backend with Spring Boot 3

Leave a comment

Discover more from The Dev World - Sergio Lema

Subscribe now to keep reading and get access to the full archive.

Continue reading