DEV Community

Cover image for When Code Breaks: How to Handle Errors Like a Pro
Akhilesh
Akhilesh

Posted on

When Code Breaks: How to Handle Errors Like a Pro

Your program has been running perfectly for ten minutes.

Then someone passes in a string where you expected a number. Or the file you're trying to read got deleted. Or the internet went down mid-request. Or a user typed nothing when your code expected input.

Your program crashes. A wall of red text appears. Everything stops.

This is not bad luck. This is normal. Errors happen in every real program. The question is not how to avoid them entirely. The question is how to catch them before they bring everything down.


What Actually Happens When Code Breaks

When Python hits a problem it cannot resolve, it raises an exception. If nothing catches that exception, the program stops and prints a traceback, that red wall of text.

numbers = [1, 2, 3]
print(numbers[10])
Enter fullscreen mode Exit fullscreen mode
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print(numbers[10])
IndexError: list index out of range
Enter fullscreen mode Exit fullscreen mode

Three pieces of information here. The file and line where it broke. The exact code that caused it. The type of error and what went wrong.

Read tracebacks from the bottom up. The last line is the actual error. Everything above it is the path Python took to get there.


Try and Except

Wrap risky code in a try block. Tell Python what to do if something goes wrong in the except block.

numbers = [1, 2, 3]

try:
    print(numbers[10])
except IndexError:
    print("That index doesn't exist in the list.")
Enter fullscreen mode Exit fullscreen mode

Output:

That index doesn't exist in the list.
Enter fullscreen mode Exit fullscreen mode

Program keeps running. No crash. No red text.

Python tries the code inside try. If an IndexError happens, it jumps to except IndexError and runs that block instead. If no error happens, the except block is skipped entirely.


Catching Different Error Types

Different problems raise different error types. You can handle each one differently.

def divide(a, b):
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        print("Cannot divide by zero.")
        return None
    except TypeError:
        print("Both inputs must be numbers.")
        return None

print(divide(10, 2))
print(divide(10, 0))
print(divide(10, "five"))
Enter fullscreen mode Exit fullscreen mode

Output:

5.0
Cannot divide by zero.
Both inputs must be numbers.
Enter fullscreen mode Exit fullscreen mode

Each except catches a specific error type. ZeroDivisionError when someone divides by zero. TypeError when the wrong type gets passed in. Both get handled cleanly without crashing.


The Errors You'll See Most Often

# NameError: used a variable that doesn't exist
print(username)   # username was never defined

# TypeError: wrong type for the operation
result = "hello" + 5

# ValueError: right type, wrong value
number = int("hello")   # "hello" can't become an integer

# IndexError: index out of range
items = [1, 2, 3]
print(items[99])

# KeyError: key doesn't exist in dictionary
data = {"name": "Alex"}
print(data["age"])

# FileNotFoundError: file doesn't exist
open("missing.txt", "r")

# ZeroDivisionError: dividing by zero
result = 10 / 0
Enter fullscreen mode Exit fullscreen mode

These seven cover probably 80% of the errors you'll encounter in the next few months. When you see one, you now know the category of problem.


Getting the Error Message

Sometimes you want to know exactly what went wrong, not just that something went wrong.

try:
    number = int("not a number")
except ValueError as e:
    print(f"Something went wrong: {e}")
Enter fullscreen mode Exit fullscreen mode

Output:

Something went wrong: invalid literal for int() with base 10: 'not a number'
Enter fullscreen mode Exit fullscreen mode

as e captures the error object. e contains the full error message. Useful for logging problems or showing the user something specific.


Else and Finally

Two optional additions to try/except that come up in real code.

else runs only if no error happened.

try:
    number = int("42")
except ValueError:
    print("That's not a valid number.")
else:
    print(f"Successfully converted: {number}")
Enter fullscreen mode Exit fullscreen mode

Output:

Successfully converted: 42
Enter fullscreen mode Exit fullscreen mode

finally runs no matter what. Error or no error.

try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("File not found.")
finally:
    print("This always runs.")
Enter fullscreen mode Exit fullscreen mode

finally is useful for cleanup code that needs to run regardless of what happened. Closing database connections. Releasing locks. Logging that an operation completed. Even if an exception occurred and was not caught, finally still runs before Python propagates the error.


Raising Your Own Errors

You can raise errors intentionally. Useful when someone uses your function in a way that doesn't make sense.

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative.")
    if age > 150:
        raise ValueError("That age is not realistic.")
    return age

try:
    set_age(-5)
except ValueError as e:
    print(f"Invalid input: {e}")
Enter fullscreen mode Exit fullscreen mode

Output:

Invalid input: Age cannot be negative.
Enter fullscreen mode Exit fullscreen mode

raise ValueError("message") creates and throws an error. The caller's try/except catches it. Your function refuses to work with bad data and tells you exactly why.

This is how professional code is written. Functions validate their inputs and raise clear errors when something is wrong. Far better than silently producing wrong answers.


A Real Pattern: Retrying on Failure

This shows up in data pipelines and API calls constantly.

def load_number(value):
    try:
        return int(value)
    except ValueError:
        return None

raw_data = ["42", "hello", "99", "abc", "17", "?"]
clean_numbers = []

for item in raw_data:
    result = load_number(item)
    if result is not None:
        clean_numbers.append(result)
    else:
        print(f"Skipped invalid value: {item}")

print(f"Clean numbers: {clean_numbers}")
Enter fullscreen mode Exit fullscreen mode

Output:

Skipped invalid value: hello
Skipped invalid value: abc
Skipped invalid value: ?
Clean numbers: [42, 99, 17]
Enter fullscreen mode Exit fullscreen mode

Real data is messy. Some values won't convert. Some files won't exist. Some network calls will fail. Handle the failures, keep the successes, move on.


The One Mistake to Avoid

Catching everything silently.

try:
    some_complicated_code()
except:
    pass
Enter fullscreen mode Exit fullscreen mode

This catches every possible error and does nothing about it. Your code fails silently. You have no idea what went wrong or where. Bugs become invisible. Do not do this.

At minimum, print the error. Better, log it. Best, handle specific error types and let unexpected ones crash loudly so you know they exist.

try:
    some_complicated_code()
except Exception as e:
    print(f"Unexpected error: {e}")
Enter fullscreen mode Exit fullscreen mode

That bare except: with no type and no message is a trap. Avoid it.


Try This

Create errors_practice.py.

Write a function called safe_divide that takes two numbers and returns the result of dividing the first by the second. Handle the case where the second number is zero. Handle the case where either input is not a number. Return None for invalid inputs and print a clear message explaining what went wrong.

Then write a function called load_profile that takes a filename, opens it, reads the JSON inside using json.load, and returns the data as a dictionary. Handle the case where the file doesn't exist. Handle the case where the file exists but is not valid JSON. Return None for failures.

Test both functions with inputs that work and inputs that should fail. Make sure your program never crashes, just handles problems cleanly and keeps going.


What's Next

Your code now handles failure gracefully. Next is modules and packages, how Python lets you use code that other people wrote, which is how you'll eventually use NumPy, Pandas, PyTorch, and every other library in this series.

Top comments (0)