KAHIBARO
Discord Login Register

4.7. Error Handling

Why Error Handling Matters

When you write code, things will go wrong. Files might not exist, user input might be invalid, network requests may fail, or your own logic might have bugs. Good backend code does not crash on the first problem. It:

Error handling is the set of tools and patterns you use to do this.

Backend systems must be especially robust, because they run for a long time and serve many users. A single unhandled error can crash the whole application or return confusing responses.

Key idea: You cannot avoid all errors.
You must design your code to expect and handle errors.

In this chapter you will learn general concepts that apply to many programming languages, and you will see examples written in a Python-like style because you will later use Python for backend development. The concepts, however, are language independent.


Types of Errors

There are several categories of errors you will meet often.

Syntax errors

These are mistakes in how the code is written. The language parser cannot understand the program at all.

Examples:

python
# Missing closing parenthesis
print("Hello"
# Wrong indentation
if x > 0:
print("positive")

Syntax errors are caught before the program runs. The interpreter or compiler stops immediately with a message like "SyntaxError".

You usually fix these during development, not by handling them at runtime.

Runtime errors (exceptions)

The code is syntactically valid, but something bad happens while it runs. In many languages this creates an exception.

Examples:

python
# Division by zero
result = 10 / 0     # ZeroDivisionError in Python
# Accessing undefined variable
print(user_age)     # NameError if user_age was never defined
# Index out of range
numbers = [1, 2, 3]
print(numbers[10])  # IndexError

These errors appear only when that line of code is executed. Different inputs may or may not trigger them.

Logical errors (bugs)

The code runs without crashing, but it does the wrong thing. The program's logic is incorrect.

Examples:

python
# Want to give 10% discount, but used addition instead of multiplication
price = 100
discounted_price = price + 0.10   # Bug: should be price * 0.90
# Using > instead of >=
if age > 18:
    print("Allowed")  # Age 18 is not allowed, maybe you wanted >=

Logical errors are the hardest to detect. Error handling patterns are less helpful here. Testing and careful design are more important.

External errors (I/O, network, environment)

These occur when your code interacts with the outside world.

Common sources:

Example, file access:

python
file = open("config.json", "r")  # Fails if file missing or no permission

Example, environment variable:

python
db_url = os.environ["DATABASE_URL"]  # Fails if variable is not set

External errors are normal in backend systems. You almost always need to handle them.


Exceptions and Control Flow

Most modern languages use exceptions to represent runtime errors. Even if the syntax differs, the main ideas are similar.

What is an exception?

An exception is a special object that represents an error or unusual situation. When the error happens, the current function stops and control jumps to the nearest error handler in the call stack.

If there is no handler, the program usually crashes.

Conceptually:

  1. Code runs normally.
  2. A problem is detected, for example division by zero.
  3. The language raises or throws an exception.
  4. The runtime looks for a piece of code that can catch or handle that exception.
  5. If found, it runs the handler. If not, the program stops with an error.

Call stack and exception propagation

Imagine this call chain:

python
def a():
    b()
def b():
    c()
def c():
    x = 10 / 0  # Error here
a()

What happens when 10 / 0 is executed:

This process is called propagation up the call stack.

You can choose at which level to handle a specific error. Sometimes it is best to handle it near where it occurred. Other times it is better to let it bubble up to a higher layer, for example a web request handler that will convert it to an HTTP response.


Basic Exception Handling Pattern

The usual pattern looks like this (in Python-like pseudocode):

python
try:
    # Code that might fail
    result = risky_operation()
except SomeErrorType as error:
    # Handle that specific error
    handle_error(error)
finally:
    # Optional, clean up resources
    cleanup()

Most languages have similar constructs, but with different keywords, such as try / catch / finally in JavaScript or Java.

The try block

Inside try you put code that might raise an exception:

python
try:
    age = int(user_input)   # Might fail for "abc"
    print(10 / age)         # Might fail for 0

If everything works, the except block is skipped and the program continues after the try / except section.

If an exception happens at some line in the try block, execution jumps out of the try block immediately. Later lines inside try are not executed.

The except (catch) block

The except block runs only if an exception of the specified type (or a compatible subtype) happens in the try block.

python
try:
    age = int(user_input)
except ValueError as e:
    print("Please enter a valid number.")

If user_input is "25", there is no error, so except is skipped.

If user_input is "abc", int("abc") raises a ValueError, so the except block runs.

You can have multiple except blocks:

python
try:
    result = 10 / int(user_input)
except ValueError:
    print("Not a number.")
except ZeroDivisionError:
    print("Cannot divide by zero.")

The first matching block is used.

Important rule: Catch specific exception types when possible.
Avoid catching all errors without understanding them.

The finally block

finally is used for cleanup code that must run whether or not there was an error.

Example:

python
file = open("data.txt", "r")
try:
    content = file.read()
    process(content)
except IOError:
    print("Could not read file.")
finally:
    file.close()  # Always executed

Even if read or process fails, file.close() still executes.

Common uses for finally:

Handling Specific vs General Exceptions

Choosing what to catch is a key skill.

Specific exceptions

Good:

python
try:
    user_id = int(user_input)
except ValueError:
    print("User ID must be a number.")

You know exactly what error you are handling and why.

Better example:

python
try:
    user = db.get_user(user_id)
except UserNotFoundError:
    return "User not found", 404

You catch a very specific problem and convert it to a precise response.

General exceptions

Many languages have a base exception type, like Exception in Python.

You can catch all exceptions:

python
try:
    risky_operation()
except Exception as e:
    log_error(e)
    return "Something went wrong"

This can be useful at the top level of your application to ensure that no error crashes the server without logging.

However, if you do this in many places, you may:

Rule: At lower levels (inside functions and modules), catch only exceptions you expect and know how to handle.
Use a broad catch only at boundaries, for example request handlers, to log and fail gracefully.


Custom Error Types

Most languages allow you to define your own error or exception types. This is very useful in backend development.

Why create custom errors?

Reasons:

Example of custom exception in Python-like code:

python
class UserNotFoundError(Exception):
    def __init__(self, user_id):
        self.user_id = user_id
        super().__init__(f"User with id {user_id} not found")

Usage:

python
def get_user(user_id):
    user = db.find_user_by_id(user_id)
    if user is None:
        raise UserNotFoundError(user_id)
    return user
try:
    user = get_user(123)
except UserNotFoundError as e:
    print(e)              # "User with id 123 not found"

Now you can have centralized handling:

python
try:
    user = get_user(user_id)
    # ...
except UserNotFoundError:
    return "User not found", 404
except PermissionDeniedError:
    return "Access denied", 403

Returning Error Codes vs Throwing Exceptions

Not all languages or styles use exceptions heavily. Sometimes functions return values that indicate success or failure.

Error codes and special values

A function might return:

Example with special value:

python
def find_user(name):
    # returns user object or None
    ...
user = find_user("alice")
if user is None:
    print("No such user")
else:
    print("Found", user)

This style can be clear if you always remember to check the result. The risk is that you forget, and the code continues in an invalid state.

Example with a (result, error) pair:

python
user, error = find_user("alice")
if error is not None:
    print("Error:", error)
else:
    print("Found", user)

Some languages, like Go, use this pattern heavily.

Exceptions vs return-based error handling

Both approaches have pros and cons.

AspectExceptionsReturn-based errors
Normal control flowClean, no extra checks on success pathsEvery call needs checks
Locality of error handlingCan be handled far away through call stackMust propagate or handle at each step
PerformanceSometimes a bit slower when raised frequentlyUsually very predictable
ExplicitnessHidden in function signatureVisible in return types / result values

In many backend projects:

Example design:

python
def find_user(user_id):
    # None means "not found", this is not an error
    # An exception means "could not access database", this is an error
    ...

Guard Clauses and Early Returns

Error handling is not only about exceptions. It is also about structuring code clearly.

One useful pattern is the guard clause. Instead of nesting if statements deeply, you exit the function early when you detect invalid input or an error condition.

Example without guard clauses:

python
def process_order(order):
    if order is not None:
        if order.is_paid:
            if order.items:
                # long processing code
                ...
            else:
                print("Order has no items")
        else:
            print("Order not paid")
    else:
        print("Order is missing")

Same logic with guard clauses:

python
def process_order(order):
    if order is None:
        print("Order is missing")
        return
    if not order.is_paid:
        print("Order not paid")
        return
    if not order.items:
        print("Order has no items")
        return
    # Here we know order is valid
    # long processing code
    ...

Both handle errors, but the guard clause version is easier to read and maintain.


Resource Cleanup and Avoiding Leaks

Backend applications often use resources that must be released:

If you do not release them, your application may:

You must ensure that cleanup always happens, even when errors occur.

Cleanup with finally

We saw a simple example before:

python
conn = open_database_connection()
try:
    data = conn.query("SELECT ...")
    process(data)
except DatabaseError:
    log("Database error")
finally:
    conn.close()  # Always executed

If query or process raises an exception, close still runs.

Using context managers (RAII-like patterns)

Many languages offer a pattern where acquiring and releasing resources is automatic.

For example, in Python:

python
with open("file.txt", "r") as file:
    content = file.read()
    process(content)
# File is closed automatically, even if an error occurs

In other languages, you might see similar constructs or patterns like RAII ("Resource Acquisition Is Initialization").

The idea is the same:

Rule: Whenever you acquire a resource, pair it with code that always releases it, even if an error occurs.


Error Messages, Logging, and Users

Backend code usually serves two audiences when something goes wrong:

  1. Developers and operators, who need technical details to debug.
  2. End users, who need simple, safe, non-technical messages.

You need to separate these concerns.

Internal vs external error information

When logging:

python
try:
    process_payment(order_id)
except PaymentGatewayError as e:
    log_error(f"Payment failed for order {order_id}: {e}")
    return "Payment could not be processed. Please try again later."

Do not show the raw database error or stack trace to users.

What to include in logs

For exceptions, logs should ideally contain:

This helps you debug production issues.

Later chapters on Logging and Monitoring will expand this.


Designing Error Handling in Layers

In a real backend you will have layers such as:

Each layer should:

Example of layered error handling:

python
class UserNotFoundError(Exception):
    ...
class DatabaseConnectionError(Exception):
    ...
def get_user_from_db(user_id):
    try:
        return db.query_user(user_id)
    except LowLevelDbConnectionError as e:
        # Convert low-level error to domain-specific one
        raise DatabaseConnectionError("Could not connect to user database") from e
def get_user_profile(user_id):
    user = get_user_from_db(user_id)
    if user is None:
        raise UserNotFoundError(user_id)
    return build_profile(user)
def handle_get_profile_request(request):
    user_id = request.params["user_id"]
    try:
        profile = get_user_profile(user_id)
        return json_response(profile, status=200)
    except UserNotFoundError:
        return json_response({"error": "User not found"}, status=404)
    except DatabaseConnectionError:
        return json_response({"error": "Service temporarily unavailable"}, status=503)

Notes:

This is the kind of structure you want in backend applications.


Common Error Handling Anti‑Patterns

Some habits will hurt your code quality and debugging experience.

Swallowing exceptions silently

Bad:

python
try:
    send_email(user)
except Exception:
    pass  # Do nothing

Now you will never know that emails are failing.

Better:

python
try:
    send_email(user)
except EmailServerError as e:
    log_error(f"Failed to send email to {user.id}: {e}")
    # maybe retry later or mark email as failed

Overusing general exception handlers

Bad:

python
try:
    do_many_things()
except Exception:
    return "Error"

You lose information about what failed. You cannot decide different actions for different errors.

Catch specific exceptions where you know what to do about them.

Using exceptions for normal control flow

Exceptions should represent exceptional situations, not common paths.

Bad:

python
try:
    user = db.get_user(user_id)
except UserNotFoundError:
    create_default_user(user_id)

If "user not found" is expected and common, often a return value is a better choice:

python
user = db.get_user(user_id)  # returns None if not found
if user is None:
    create_default_user(user_id)

Ignoring function contracts

If a function can raise an exception or return a special value, the caller must respect that.

Bad:

python
# find_user might return None
user = find_user("alice")
print(user.name)  # Might raise an attribute error

Good:

python
user = find_user("alice")
if user is None:
    print("User not found")
else:
    print(user.name)

Practice Exercises

Try to solve these in a language of your choice, ideally Python, but focus on the concepts.

  1. Safe division function

Write a function safe_divide(a, b) that:

Then write code that calls safe_divide and prints either the result or "Invalid division" based on whether the result is None.

  1. File reading with error handling

Write a function read_config(path) that:

Make sure the file is always closed, even on errors.

  1. Custom error type

Define a custom exception InvalidAgeError. Write a function register_user(name, age) that:

Write code that calls register_user for several test values and catches InvalidAgeError to print friendly error messages.

  1. Layered error handling sketch

Design three functions:

Implement these functions and test different scenarios.

Working through these will make the abstract ideas concrete, and you will be ready to apply them in real backend code later in the course.

Views: 9

Comments

Please login to add a comment.

Don't have an account? Register now!