Learn

Home

Image represents a media kit with boilerplate, logos and more

Guide

Error Handling: A Guide to Preventing Unexpected Crashes

Proper error handling ensures your programs can gracefully navigate unforeseen situations without crashing or compromising the user experience.

Table of Contents


Introduction


Imagine this: You've just released your latest application update. Everything works perfectly in your development environment, but as soon as users start interacting with it, unexpected errors cause the app to crash. Frustrating, isn't it? We've all been there.


As developers, we know that error handling is more than a box to check, it's a cornerstone of mature software development. Proper error handling ensures your programs can gracefully navigate unforeseen situations without crashing or compromising the user experience. It involves anticipating potential issues, detecting anomalies, and responding effectively during program execution.


Neglecting error handling can have disastrous consequences. Crashes not only frustrate users but can also lead to a loss of trust and potentially cause physical or financial harm. Beyond the immediate fallout, inadequate error handling can result in:


  • Data corruption
  • Loss of important information
  • Unintentional exposure of sensitive data


These scenarios pose significant security risks, especially in industries where software reliability is non-negotiable, like finance, healthcare, or transportation. In such environments, software failures can have life-altering implications.


But here's the good news: With robust error-handling techniques, you can prevent these outcomes and build applications that stand resilient in the face of the unexpected. In this guide, we'll explore how to leverage these techniques using Python's try and except syntax, powerful tools for implementing exception handling. You'll learn how to prevent unexpected runtime errors and create systems that not only survive but thrive under pressure.


And while we'll demonstrate these practices in Python, the principles you'll gain are universal, applicable to any programming language you use. So, whether you're a seasoned developer or just starting your coding journey, mastering error handling is a skill that will elevate your software development to the next level.



Ready to build more reliable applications? Let's dive in.


What are Common Error-Handling Techniques?


Let’s begin by looking at the most common error-handling use cases and techniques.


try and except blocks


The most common method for handling errors in Python is to use try and except blocks. The exact syntax wording differs slightly for languages supporting exception handling, such as Java, JavaScript, PHP, Ruby, and C++. For example, some may refer to this technique as try-catch. This technique allows programmers to isolate potentially error-prone code—such as constants lacking initialization, invalid data types, or namespace issues—and specify actions to take if an exception occurs.


try:

    result = 10 / 0

except ZeroDivisionError as e:

    print("Error: %s", e)


In the example above, the try block contains code that might throw exceptions, while the except block (or catch block) defines how to handle specific exceptions, such as ZeroDivisionError. These try-except blocks prevent the program from crashing and allow it to recover from the error.


Error codes


While traditional error codes were popular in languages like Bash, C, and Pascal, modern programming practices favor exceptions for better readability. However, Python developers can create custom exception classes to categorize and identify specific errors akin to error codes.


# CustomError inherits from the base Python exception class
class CustomError(Exception):
    def __init__(self, message):
        super().__init__(message)

try:
    raise CustomError("An error occurred")
except CustomError as e:
    print("Error %s", {e})


Custom exception classes enhance error identification while preserving Python’s clean, readable error-handling mechanisms. Across all programming languages, using custom error objects helps with troubleshooting.


Logging


Logging is an indispensable debugging tool that monitors production-system errors. By recording error details—such as call stack location, callback information, error conditions, and expected return values—developers can gain valuable insights into the root causes of issues.


import logging

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

logger = logging.getLogger(__name__)


try:

    result = 10 / 0

except ZeroDivisionError as e:

    logger.error("An error occurred: %s", e)


In this example, the logging module captures the error details, including timestamps and severity levels, helping developers trace and resolve issues effectively.


Validation


Input validation is a proactive approach to error handling. Developers can avoid many types of errors by ensuring data meets specific criteria before processing.


def validate_age(age: int) -> None:

    if not isinstance(age, int) or age < 0:

        raise ValueError("Age must be a positive integer")

    print(f"Processing age: {age}")


try:

    validate_age("twenty")

except ValueError as e:

    print("Validation Error: %s", e)


Here, the process_age function checks the validity of its input, raising a ValueError if the criteria are unmet. Validating parameters like this helps catch errors early, reducing the likelihood of unexpected behavior later in the program.


What are Best Practices for Error Handling?


If you’re new to error handling, we’ve provided several best practices to help you get started.


#1: Provide informative error messages


When a new error occurs, handle it to minimize disruptions to the user. Providing clear and informative error messages helps users understand what went wrong and which steps to take next.


try:

    value = int("abc")

except ValueError as e:

    print("Invalid input. Please enter a number.")


A user-friendly error message, as shown above, improves the overall experience and mitigates confusion.


#2: Avoid silent failures


Catching exceptions without taking any action can make debugging and maintenance extremely difficult. Ensure that every exception is logged, handled, or propagated appropriately.


try:

    result = 10 / 0

except ZeroDivisionError:

    pass  # Don't do this!


Instead of failing silently with pass, capture the error for the user by logging it to a database or log aggregator. 


#3: Distinguish between recoverable and unrecoverable errors


Not all errors are created equal. Some errors may be recoverable, meaning the program can handle them and continue execution. Others, such as invalid configurations or missing critical resources, require immediate termination. 


Unrecoverable errors prevent the program from continuing meaningfully. For example:


import json

from sys import exit


try:

    with open("config.json", "r") as file:

        config = json.load(file)

        if not config.get("api_key"):

            raise ValueError("Missing API key in configuration")

except FileNotFoundError:

    print("Configuration file not found. Exiting.")

    exit(1)

except ValueError as e:

    print("Configuration error: %s", e)

    exit(1)


If the config.json file is missing or improperly configured, the program terminates (as it should) because it cannot function without valid configuration settings.


#4: Implement retry mechanisms for transient errors


Network issues are often transient and complex to reproduce​​. Despite this, they can frequently be resolved with a retry mechanism. Using libraries like Tenacity can simplify this process.


from tenacity import retry, wait_fixed, stop_after_attempt


@retry(stop=stop_after_attempt(4), wait=wait_fixed(1))

def fetch_data():

    print("Attempting to fetch data...")

    raise TimeoutError("Simulated temporary network issue")


try:

    fetch_data()

except TimeoutError as e:

    print(f"Failed after retries: {e}")


The retry decorator function will attempt to execute the method at least four times, waiting one second between retries. 


#5: Consider using a centralized error-handling system


Centralized error handling can streamline the management of exceptions across a large application. Frameworks like Flask or Django often provide middleware, such as the errorhandler decorator below, that collects all exceptions regardless of where they are thrown in the Flask application.


from flask import Flask, jsonify


app = Flask(__name__)


@app.errorhandler(Exception)

def handle_exception(e):

    return jsonify(error=str(e)), 500


@app.route('/')

def index():

    raise ValueError("An example error")


if __name__ == "__main__":

    app.run()


By consolidating error handling, developers can ensure consistent responses and reduce redundancy in their code.


Error Handling in Different Contexts


Depending on your software development context, you may have different approaches to error handling. Let’s consider web applications, APIs, and databases.


Web applications


Error handling in web applications is critical for ensuring a smooth user experience. Flask, Django, and FastAPI provide built-in tools for handling exceptions and returning meaningful HTML or HTTP responses. For example, with FastAPI, custom responses can be created for HTTP errors like 500 to guide users effectively.


import uvicorn

from fastapi import FastAPI, HTTPException

from fastapi.responses import JSONResponse


app = FastAPI()


@app.exception_handler(Exception)

async def global_exception_handler(request, exc):

    return JSONResponse(

        status_code=500,

        content={"error": str(exc)}

    )


@app.get("/")

async def read_root():

    raise ValueError("An example error")


if __name__ == "__main__":

    uvicorn.run(app, host="0.0.0.0", port=8000)


APIs


In APIs, error handling ensures clients receive appropriate status codes and error messages. For example, with Flask, using HTTP status codes like 400 for bad requests or 500 for server errors helps clients understand the nature of the problem.


@app.route('/api/resource')

def get_resource():

    try:

        # Simulate resource fetching

        raise ValueError("Invalid resource ID")

    except ValueError as e:

        return jsonify(error=str(e)), 400


Databases


Database errors, such as connection errors or query failures, need special attention. Using ORM libraries like SQLAlchemy allows developers to handle database-specific exceptions gracefully.


from sqlalchemy.exc import SQLAlchemyError

try:

    result = db.session.query(SomeModel).all()

except SQLAlchemyError as e:

    print("Database error, %s", e)


Tools and Libraries


Below is a list of helpful error-handling resources for Python developers. Most programming languages have equivalent tools.


Logging and monitoring

  • Logging: Python’s built-in logging module helps record error details.
  • Sentry: A popular tool for real-time error tracking and monitoring.
  • Exceptionite: An exception library focusing on readability.


Retry mechanisms

  • Retrying: A library for implementing retry logic.
  • Tenacity: A modern alternative for retrying transient errors.


Framework-specific tools

  • Flask: Provides decorators and middleware for centralized error handling.
  • Django: Offers error handling through middleware and custom exception handling.
  • FastAPI: Offers capabilities similar to those of the frameworks above.


Testing tools

  • Pytest: Useful for testing error-handling scenarios.
  • Unittest: Python’s built-in unit-test library.
  • Lettuce: A behavior-driven testing framework.


By leveraging these tools and libraries, developers can enhance their error-handling capabilities and build more resilient applications.


Conclusion


Robust error handling is the cornerstone of building reliable, secure, and user-friendly software. Your code may get past the compiler, but that only means it’s free of syntax errors. That doesn’t mean your code will be free of errors at runtime.


Developers can effectively manage unexpected scenarios while maintaining software stability by adopting try and except blocks, proactive validation, and centralized error management. Best practices like providing informative error messages, avoiding silent failures, distinguishing between recoverable and unrecoverable errors, and leveraging retry mechanisms ensure errors are addressed systematically and thoughtfully.


Implementing these principles reduces the likelihood of crashes and data loss, fostering user trust and satisfaction. As software engineers, proactively addressing potential errors in your code is an investment in quality and reliability that will pay dividends over the lifetime of your applications. Take the time to integrate robust error handling into your development process and watch your software become more resilient to the unexpected.


SonarQube is an essential tool for developers aiming to strengthen error handling in their applications. By analyzing your codebase, it identifies potential issues, such as unhandled exceptions, insufficient logging, or overly complex error-handling logic, that could compromise reliability and security. SonarQube's actionable insights and user-friendly dashboards help teams pinpoint areas for improvement and enforce best practices, ensuring that your error-handling strategies are robust and maintainable. Integrating SonarQube into your development process not only reduces the risk of unexpected crashes but also fosters a proactive approach to building resilient and user-friendly software.