Good error handling makes code robust and debuggable. Here's how to do it right in Python.

Basic try/except

try:
    result = risky_operation()
except SomeError:
    handle_error()

Catching Specific Exceptions

Always catch specific exceptions:

# Bad - catches everything including KeyboardInterrupt
try:
    data = json.loads(text)
except:
    pass
 
# Better - catches only what you expect
try:
    data = json.loads(text)
except json.JSONDecodeError as e:
    print(f"Invalid JSON: {e}")

Multiple Exceptions

Handle different errors differently:

try:
    with open(path) as f:
        data = json.load(f)
except FileNotFoundError:
    data = {}  # Use default
except json.JSONDecodeError:
    raise ValueError(f"Invalid JSON in {path}")
except PermissionError:
    raise  # Re-raise as-is

Or catch multiple as one:

try:
    process(data)
except (ValueError, TypeError) as e:
    print(f"Invalid input: {e}")

The else Clause

Runs only if no exception occurred:

try:
    result = fetch_data()
except NetworkError:
    result = None
else:
    # Only runs if fetch succeeded
    save_to_cache(result)

The finally Clause

Always runs, even if there's an exception:

try:
    conn = open_connection()
    result = conn.query(sql)
except DatabaseError:
    log_error()
    raise
finally:
    conn.close()  # Always close

Re-raising Exceptions

Keep the original traceback:

try:
    process()
except ValueError:
    log("Processing failed")
    raise  # Re-raises with original traceback

Exception Chaining

Show the cause of an error:

try:
    data = fetch_from_api()
except RequestError as e:
    raise DataError("Failed to get data") from e
 
# Traceback shows both exceptions

Custom Exceptions

Create meaningful exceptions for your domain:

class AppError(Exception):
    """Base exception for our application."""
    pass
 
class ValidationError(AppError):
    """Input validation failed."""
    pass
 
class NotFoundError(AppError):
    """Resource not found."""
    pass
 
class AuthenticationError(AppError):
    """Authentication failed."""
    pass

With extra context:

class APIError(Exception):
    def __init__(self, message, status_code=None, response=None):
        super().__init__(message)
        self.status_code = status_code
        self.response = response
 
# Usage
raise APIError("Request failed", status_code=500, response=resp)
 
# Handling
except APIError as e:
    if e.status_code == 429:
        wait_and_retry()

Exception Hierarchies

Design exceptions for flexible catching:

class StorageError(Exception):
    """Base for storage errors."""
    pass
 
class ConnectionError(StorageError):
    """Can't connect to storage."""
    pass
 
class ReadError(StorageError):
    """Can't read from storage."""
    pass
 
class WriteError(StorageError):
    """Can't write to storage."""
    pass
 
# Users can catch specific or broad
try:
    save_data()
except WriteError:
    # Handle writes specifically
except StorageError:
    # Or catch all storage issues

EAFP vs LBYL

EAFP (Easier to Ask Forgiveness than Permission):

# Pythonic
try:
    value = dictionary[key]
except KeyError:
    value = default

LBYL (Look Before You Leap):

# Less Pythonic
if key in dictionary:
    value = dictionary[key]
else:
    value = default

EAFP is usually preferred in Python. But use LBYL for expensive checks or when exceptions have side effects.

Context Managers for Cleanup

Better than try/finally:

# Instead of try/finally
try:
    f = open("file.txt")
    data = f.read()
finally:
    f.close()
 
# Use context manager
with open("file.txt") as f:
    data = f.read()

Logging Exceptions

Log with full traceback:

import logging
 
try:
    process()
except Exception:
    logging.exception("Processing failed")  # Includes traceback
    raise

Suppressing Exceptions

When you really want to ignore errors:

from contextlib import suppress
 
# Instead of try/except/pass
with suppress(FileNotFoundError):
    os.remove("temp.txt")

Anti-Patterns

Don't catch Exception blindly:

# Bad
try:
    do_something()
except Exception:
    pass  # Swallows all errors

Don't use exceptions for flow control:

# Bad
try:
    items[index]
except IndexError:
    return None
 
# Better
if index < len(items):
    return items[index]
return None

Don't ignore exceptions silently:

# Bad
except SomeError:
    pass
 
# Better - at least log
except SomeError:
    logger.warning("Operation failed, continuing")

Quick Reference

try:
    risky_code()
except SpecificError as e:
    handle_specific(e)
except (Error1, Error2):
    handle_multiple()
except Exception as e:
    log_unexpected(e)
    raise
else:
    on_success()
finally:
    cleanup()

Good error handling is about being intentional. Catch what you expect, let unexpected errors propagate, and always preserve context for debugging.

React to this post: