Error Handling with try/except

· 7 min read · Updated March 7, 2026 · beginner
error-handling exceptions try-except debugging

Errors happen in every program. A file might not exist, a network request might time out, or a user might enter invalid input. Without error handling, your program crashes and displays a confusing traceback. With error handling, you can respond gracefully and keep your program running.

Python provides a powerful exception handling mechanism that lets you catch errors and respond to them in a controlled way.

Catching Exceptions with try and except

The try block contains code that might fail. The except block handles the error if one occurs:

try:
    with open("config.txt", "r") as file:
        data = file.read()
except FileNotFoundError:
    print("The configuration file is missing.")

If the file doesn’t exist, Python raises a FileNotFoundError, which the except block catches. Instead of crashing, the program prints a friendly message and continues.

The except block only runs when an exception occurs. If the file exists and reads successfully, the except block is skipped entirely.

How try/except Works

When Python executes a try block:

  1. The code between try and the first except runs normally
  2. If no exception occurs, all except blocks are skipped
  3. If an exception occurs, Python stops executing the try block and looks for a matching except block
  4. If a matching handler is found, it executes and then continues after the try/except structure
  5. If no handler matches, the exception propagates up and crashes the program

Handling Multiple Exception Types

A single try block can raise different types of exceptions. Handle each one separately:

try:
    with open("data.txt", "r") as file:
        number = int(file.read().strip())
        result = 100 / number
except FileNotFoundError:
    print("The data file doesn't exist.")
except ValueError:
    print("The file doesn't contain a valid number.")
except ZeroDivisionError:
    print("Cannot divide by zero.")

Python evaluates the except clauses from top to bottom and executes the first matching one. Order matters—if you put a general exception type before a specific one, the general handler catches everything.

Catching Multiple Exceptions Together

When different exceptions need the same handling, group them in a single except clause:

try:
    result = int(user_input) / 0
except (ValueError, ZeroDivisionError):
    print("Invalid input or division by zero.")

This is cleaner than writing separate handlers with duplicate code.

Accessing the Exception Object

Sometimes you need details about what went wrong. The as keyword gives you access to the exception instance:

try:
    with open("config.json", "r") as file:
        config = json.load(file)
except FileNotFoundError as e:
    print(f"File missing: {e.filename}")
except json.JSONDecodeError as e:
    print(f"Invalid JSON at position {e.pos}: {e.msg}")

Exception objects contain useful attributes. For FileNotFoundError, you can access filename. For JSONDecodeError, you get pos, msg, and lineno.

The else Clause

The else block runs only when the try block completes without raising an exception. Use it for code that should execute on success:

try:
    with open("data.txt", "r") as file:
        content = file.read()
except FileNotFoundError:
    print("File not found.")
else:
    print(f"Successfully read {len(content)} characters.")

The else clause separates the success path from error handling. This makes your code easier to read—you know exactly which code runs on success and which runs on failure.

The finally Clause

The finally block runs regardless of whether an exception occurred. Use it for cleanup code that must execute:

file = None
try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("File not found.")
finally:
    if file:
        file.close()
    print("Cleanup complete.")

The finally block is useful for releasing resources, closing database connections, or unlocking files. It runs even if an exception is raised inside the try block.

Using Context Managers Instead

The with statement is a cleaner way to handle file cleanup:

try:
    with open("data.txt", "r") as file:
        content = file.read()
except FileNotFoundError:
    print("File not found.")

Context managers automatically close the file when the block exits, even if an exception occurs. Use with whenever possible—it’s more concise and less error-prone than manual cleanup.

Raising Exceptions

You can raise exceptions yourself using the raise keyword. This is useful for validating input or signaling errors:

def divide(a, b):
    if b == 0:
        raise ValueError("Divisor cannot be zero.")
    return a / b

try:
    result = divide(10, 0)
except ValueError as e:
    print(f"Error: {e}")

Re-raising Exceptions

Sometimes you want to catch an exception, do something with it (like log it), and then let it continue propagating:

try:
    risky_operation()
except Exception as e:
    print(f"Something went wrong: {e}")
    raise  # Re-raises the same exception

The bare raise statement re-raises the caught exception.

Exception Chaining

You can chain exceptions to show cause and effect:

try:
    with open("config.txt", "r") as file:
        config = json.load(file)
except FileNotFoundError as exc:
    raise RuntimeError("Configuration is required") from exc

The from exc part creates an explicit exception chain. Use from None to suppress the original exception:

raise RuntimeError("Configuration is required") from None

Custom Exceptions

For domain-specific errors, create custom exception classes:

class InvalidAgeError(Exception):
    """Raised when age is not a valid value."""
    pass

def set_age(age):
    if not isinstance(age, int):
        raise InvalidAgeError("Age must be an integer.")
    if age < 0:
        raise InvalidAgeError("Age cannot be negative.")
    return age

try:
    set_age(-5)
except InvalidAgeError as e:
    print(f"Invalid age: {e}")

Name custom exceptions with “Error” at the end—a convention that makes your code recognizable to other Python developers.

Custom Exceptions with Extra Data

Custom exceptions can carry additional information:

class ValidationError(Exception):
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

try:
    raise ValidationError("email", "Invalid format")
except ValidationError as e:
    print(f"Problem in field: {e.field}")
    print(f"Details: {e.message}")

Best Practices

Be Specific with Exception Types

Avoid catching all exceptions with a bare except:

# Good - catches specific exception
try:
    data = json.loads(user_input)
except json.JSONDecodeError:
    print("Invalid JSON")

# Avoid - catches everything including KeyboardInterrupt
try:
    data = json.loads(user_input)
except:
    print("Invalid JSON")

The bare except also catches KeyboardInterrupt (Ctrl+C) and SystemExit, which users normally expect to terminate the program.

Don’t Use Exceptions for Flow Control

If you can check a condition first, do so instead of using exceptions:

# Better - check first
if key in my_dict:
    value = my_dict[key]
else:
    value = default

# Works but less efficient
try:
    value = my_dict[key]
except KeyError:
    value = default

Exceptions are relatively expensive in Python because building the traceback takes time. Use them for genuinely exceptional situations, not normal control flow.

Use finally for Cleanup

Always clean up resources, even when exceptions occur:

connection = None
try:
    connection = connect_to_database()
    result = connection.query("SELECT * FROM users")
finally:
    if connection:
        connection.close()

Or better, use a context manager that handles cleanup automatically.

Common Exception Types

Knowing what exceptions to expect helps you handle them properly:

ExceptionWhen it occurs
FileNotFoundErrorFile doesn’t exist
PermissionErrorNo read/write permission
ValueErrorRight type, wrong value
TypeErrorWrong type entirely
KeyErrorDictionary key not found
IndexErrorList index out of range
ZeroDivisionErrorDivision by zero
ConnectionErrorNetwork connection failed

A Practical Example

Here’s a function that parses user input with proper error handling:

def parse_age(input_string):
    """Parse a string into an age integer."""
    try:
        age = int(input_string.strip())
    except ValueError:
        raise ValueError("Please enter a valid whole number.")
    
    if age < 0:
        raise ValueError("Age cannot be negative.")
    if age > 150:
        raise ValueError("Please enter a realistic age.")
    
    return age

# Usage
while True:
    user_input = input("Enter your age: ")
    try:
        age = parse_age(user_input)
        print(f"You are {age} years old.")
        break
    except ValueError as e:
        print(f"Invalid input: {e}")

This example demonstrates input validation, custom error messages, and looping until valid input is received.

Conclusion

Error handling is essential for writing robust Python programs. The try/except mechanism lets you catch and handle exceptions gracefully. Use else to separate success logic from error handling, and finally for cleanup that must always run. Raise exceptions yourself when something goes wrong, and create custom exception classes for domain-specific errors.

Remember to be specific about which exceptions you catch, avoid using exceptions for normal flow control, and use context managers for resource cleanup. These practices will help you write code that’s easier to maintain and debug.

Next Steps

Now that you understand error handling, explore these related topics:

  • The logging module for recording errors in production
  • The traceback module for detailed error analysis
  • Context managers and the contextlib module
  • Testing exception handling with pytest