Skip to main content

Flask: Custom Error Pages

Flask: Custom Error Pages

Custom error pages in Flask enhance user experience by providing meaningful, branded responses for HTTP errors like 404 (Not Found) or 500 (Internal Server Error). Flask’s flexible error handling allows developers to define custom templates and logic for specific error codes, improving usability and professionalism. This tutorial explores creating custom error pages in Flask, covering setup, template rendering, error handling, and best practices for user-friendly error responses.


01. Why Use Custom Error Pages in Flask?

Default error pages are often generic and uninformative, potentially frustrating users. Custom error pages provide clear messaging, consistent branding, and actionable guidance (e.g., navigation links). Flask’s error handling system makes it easy to catch HTTP errors, log them, and render tailored templates, ensuring a polished user experience and better debugging capabilities.

Example: Basic Custom 404 Error Page

from flask import Flask, render_template

app = Flask(__name__)

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

@app.route('/')
def index():
    return "Home Page"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Template (templates/404.html):

<!DOCTYPE html>
<html>
<head>
    <title>Page Not Found</title>
</head>
<body>
    <h1>404 - Page Not Found</h1>
    <p>Sorry, the page you are looking for does not exist.</p>
    <a href="/">Return to Home</a>
</body>
</html>

Output:

* Running on http://127.0.0.1:5000
(Accessing /nonexistent: Renders custom 404.html page)

Explanation:

  • @app.errorhandler(404) - Registers a handler for 404 errors.
  • render_template - Renders a custom HTML template with a 404 status code.

02. Key Techniques for Custom Error Pages

Flask supports various techniques to create and manage custom error pages, from simple template rendering to advanced logging and dynamic content. These techniques ensure flexibility and robustness. The table below summarizes key techniques and their applications:

Technique Description Use Case
Error Handlers Define handlers for specific HTTP codes 404, 500, 403 errors
Custom Templates Render branded HTML pages User-friendly responses
Exception Handling Catch custom or generic exceptions Application-specific errors
Logging Errors Log error details for debugging Monitor and diagnose issues
Dynamic Content Add context to error pages Personalized error messages


2.1 Handling Common HTTP Errors

Example: Custom 404 and 500 Error Pages

from flask import Flask, render_template

app = Flask(__name__)

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

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

@app.route('/')
def index():
    return "Home Page"

@app.route('/crash')
def crash():
    raise Exception("Test crash")

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Template (templates/404.html):

<!DOCTYPE html>
<html>
<head>
    <title>Not Found</title>
</head>
<body>
    <h1>404 - Not Found</h1>
    <p>Oops, this page doesn't exist.</p>
    <a href="/">Go Home</a>
</body>
</html>

Template (templates/500.html):

<!DOCTYPE html>
<html>
<head>
    <title>Server Error</title>
</head>
<body>
    <h1>500 - Server Error</h1>
    <p>Something went wrong on our end. Please try again later.</p>
    <a href="/">Go Home</a>
</body>
</html>

Output:

* Running on http://127.0.0.1:5000
(Accessing /nonexistent: Renders 404.html)
(Accessing /crash: Renders 500.html)

Explanation:

  • Handles 404 and 500 errors with distinct templates.
  • 500 handler catches unhandled exceptions in routes.

2.2 Handling Custom Exceptions

Example: Custom Exception Handling

from flask import Flask, render_template

app = Flask(__name__)

class CustomError(Exception):
    pass

@app.errorhandler(CustomError)
def handle_custom_error(error):
    return render_template('custom_error.html', message=str(error)), 400

@app.route('/custom')
def custom():
    raise CustomError("Invalid input provided")

@app.route('/')
def index():
    return "Home Page"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Template (templates/custom_error.html):

<!DOCTYPE html>
<html>
<head>
    <title>Custom Error</title>
</head>
<body>
    <h1>Custom Error</h1>
    <p>{{ message }}</p>
    <a href="/">Go Home</a>
</body>
</html>

Output:

* Running on http://127.0.0.1:5000
(Accessing /custom: Renders custom_error.html with "Invalid input provided")

Explanation:

  • Defines a custom exception class and corresponding error handler.
  • Passes error details to the template for dynamic rendering.

2.3 Logging Errors with Middleware

Example: Error Logging Middleware

from flask import Flask, render_template
from werkzeug.wrappers import Request, Response
import logging

app = Flask(__name__)

logging.basicConfig(level=logging.ERROR)
logger = logging.getLogger(__name__)

class ErrorLoggingMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        try:
            return self.wsgi_app(environ, start_response)
        except Exception as e:
            logger.error(f"Error in {request.method} {request.path}: {str(e)}", exc_info=True)
            res = Response(render_template('500.html'), status=500, mimetype='text/html')
            return res(environ, start_response)

app.wsgi_app = ErrorLoggingMiddleware(app.wsgi_app)

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

@app.route('/')
def index():
    return "Home Page"

@app.route('/crash')
def crash():
    raise Exception("Test crash")

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Template (templates/500.html):

<!DOCTYPE html>
<html>
<head>
    <title>Server Error</title>
</head>
<body>
    <h1>500 - Server Error</h1>
    <p>Something broke! We're working on it.</p>
    <a href="/">Go Home</a>
</body>
</html>

Output:

* Running on http://127.0.0.1:5000
ERROR:__main__:Error in GET /crash: Test crash
(Accessing /crash: Renders 500.html)

Explanation:

  • Middleware logs exceptions with stack traces and renders a 500 error page.
  • Combines logging with custom error rendering for observability.

2.4 Dynamic Error Pages

Example: Dynamic 403 Error Page

from flask import Flask, render_template, abort

app = Flask(__name__)

@app.errorhandler(403)
def forbidden(error):
    return render_template('403.html', reason="Access denied due to insufficient permissions"), 403

@app.route('/restricted')
def restricted():
    abort(403)  # Simulate forbidden access

@app.route('/')
def index():
    return "Home Page"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Template (templates/403.html):

<!DOCTYPE html>
<html>
<head>
    <title>Forbidden</title>
</head>
<body>
    <h1>403 - Forbidden</h1>
    <p>{{ reason }}</p>
    <a href="/">Go Home</a>
</body>
</html>

Output:

* Running on http://127.0.0.1:5000
(Accessing /restricted: Renders 403.html with "Access denied due to insufficient permissions")

Explanation:

  • Passes dynamic content (reason) to the template.
  • Enhances user experience with context-specific error messages.

2.5 Incorrect Error Handling

Example: Missing Status Code

from flask import Flask, render_template

app = Flask(__name__)

@app.errorhandler(404)
def page_not_found(error):
    return render_template('404.html')  # Missing status code

@app.route('/')
def index():
    return "Home Page"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
(Accessing /nonexistent: Renders 404.html but with 200 OK status)

Explanation:

  • Omitting the status code defaults to 200, which is misleading for errors.
  • Solution: Always return the correct status code (e.g., return ..., 404).

03. Effective Usage

3.1 Recommended Practices

  • Combine error handlers with middleware for logging and consistent error pages.

Example: Comprehensive Error Handling

from flask import Flask, render_template, abort
from werkzeug.wrappers import Request, Response
import logging
from logging.handlers import RotatingFileHandler

app = Flask(__name__)

# Setup logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR)
handler = RotatingFileHandler('errors.log', maxBytes=1000000, backupCount=5)
handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
logger.addHandler(handler)

class CustomError(Exception):
    pass

# Middleware for error logging
class ErrorLoggingMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        try:
            return self.wsgi_app(environ, start_response)
        except Exception as e:
            logger.error(f"Error in {request.method} {request.path}: {str(e)}", exc_info=True)
            res = Response(render_template('500.html'), status=500, mimetype='text/html')
            return res(environ, start_response)

app.wsgi_app = ErrorLoggingMiddleware(app.wsgi_app)

# Error handlers
@app.errorhandler(404)
def page_not_found(error):
    return render_template('404.html'), 404

@app.errorhandler(403)
def forbidden(error):
    return render_template('403.html', reason="Access restricted"), 403

@app.errorhandler(CustomError)
def handle_custom_error(error):
    return render_template('custom_error.html', message=str(error)), 400

# Routes
@app.route('/')
def index():
    return "Home Page"

@app.route('/restricted')
def restricted():
    abort(403)

@app.route('/custom')
def custom():
    raise CustomError("Invalid action")

@app.route('/crash')
def crash():
    raise Exception("Test crash")

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Template (templates/404.html):

<!DOCTYPE html>
<html>
<head>
    <title>Not Found</title>
</head>
<body>
    <h1>404 - Not Found</h1>
    <p>This page doesn't exist.</p>
    <a href="/">Go Home</a>
</body>
</html>

Template (templates/403.html):

<!DOCTYPE html>
<html>
<head>
    <title>Forbidden</title>
</head>
<body>
    <h1>403 - Forbidden</h1>
    <p>{{ reason }}</p>
    <a href="/">Go Home</a>
</body>
</html>

Template (templates/500.html):

<!DOCTYPE html>
<html>
<head>
    <title>Server Error</title>
</head>
<body>
    <h1>500 - Server Error</h1>
    <p>Something went wrong. Please try again later.</p>
    <a href="/">Go Home</a>
</body>
</html>

Template (templates/custom_error.html):

<!DOCTYPE html>
<html>
<head>
    <title>Custom Error</title>
</head>
<body>
    <h1>Custom Error</h1>
    <p>{{ message }}</p>
    <a href="/">Go Home</a>
</body>
</html>

Output (errors.log):

2025-05-12 10:00:00,123 - Error in GET /crash: Test crash

Output (Browser):

* Running on http://127.0.0.1:5000
/nonexistent: Renders 404.html
/restricted: Renders 403.html
/custom: Renders custom_error.html
/crash: Renders 500.html
  • Handles multiple HTTP errors and custom exceptions.
  • Logs errors to a rotating file for debugging.
  • Renders user-friendly, dynamic error pages.

3.2 Practices to Avoid

  • Avoid exposing sensitive error details to users.

Example: Exposing Error Details

from flask import Flask

app = Flask(__name__)

@app.errorhandler(500)
def internal_server_error(error):
    return str(error), 500  # Exposes stack trace

@app.route('/crash')
def crash():
    raise Exception("Test crash")

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
(Accessing /crash: Shows stack trace in browser)
  • Exposing stack traces risks security vulnerabilities.
  • Solution: Render a generic error page and log details internally.

04. Common Use Cases

4.1 Handling 404 for Missing Resources

Provide a friendly page for invalid URLs.

Example: 404 for Missing Resources

from flask import Flask, render_template

app = Flask(__name__)

@app.errorhandler(404)
def page_not_found(error):
    return render_template('404.html', path=request.path), 404

@app.route('/')
def index():
    return "Home Page"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Template (templates/404.html):

<!DOCTYPE html>
<html>
<head>
    <title>Not Found</title>
</head>
<body>
    <h1>404 - Not Found</h1>
    <p>The page at {{ path }} does not exist.</p>
    <a href="/">Go Home</a>
</body>
</html>

Output:

* Running on http://127.0.0.1:5000
(Accessing /missing: Renders 404.html with "The page at /missing does not exist")

Explanation:

  • Informs users about the invalid URL with dynamic path info.
  • Encourages navigation back to the home page.

4.2 Handling Authentication Errors

Display custom pages for 401/403 errors due to authentication issues.

Example: 401 Unauthorized Error

from flask import Flask, render_template, abort

app = Flask(__name__)

@app.errorhandler(401)
def unauthorized(error):
    return render_template('401.html'), 401

@app.route('/login')
def login():
    abort(401)  # Simulate unauthorized access

@app.route('/')
def index():
    return "Home Page"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Template (templates/401.html):

<!DOCTYPE html>
<html>
<head>
    <title>Unauthorized</title>
</head>
<body>
    <h1>401 - Unauthorized</h1>
    <p>Please log in to access this page.</p>
    <a href="/">Go Home</a>
</body>
</html>

Output:

* Running on http://127.0.0.1:5000
(Accessing /login: Renders 401.html)

Explanation:

  • Guides users to log in for unauthorized access attempts.
  • Maintains consistent branding and navigation.

Conclusion

Custom error pages in Flask improve user experience and debugging capabilities. Key takeaways:

  • Use @app.errorhandler for HTTP errors and custom exceptions.
  • Render branded templates with dynamic content for context.
  • Integrate middleware for error logging and consistent 500 handling.
  • Avoid exposing sensitive error details to users.

With these practices, you can create user-friendly, professional error pages for robust Flask applications!

Comments