Web Analytics

Raising Exceptions

Intermediate ~30 min read

Sometimes you need to signal an error yourself - when validation fails, when preconditions aren't met, or when something unexpected happens. The raise statement lets you throw exceptions, create custom exception types for your application, and chain exceptions to preserve error context!

Basic Raise Statement

Use raise ExceptionType("message") to throw an exception. Choose the appropriate built-in exception type - ValueError for invalid values, TypeError for wrong types, KeyError for missing keys. Always include a descriptive message.

Output
Click Run to execute your code
When to Raise Which Exception:
ValueError - Right type, wrong value: raise ValueError("age must be positive")
TypeError - Wrong type: raise TypeError("expected string")
KeyError - Missing key: raise KeyError("config missing 'host'")
RuntimeError - General runtime issue: raise RuntimeError("state corrupted")
NotImplementedError - Subclass must override: raise NotImplementedError

Re-raising Exceptions

Sometimes you want to catch an exception, do something (like logging), and then let it propagate. Use bare raise (without arguments) to re-raise the current exception. This preserves the original traceback, which is crucial for debugging.

Output
Click Run to execute your code
Re-raise Best Practices:
raise - Bare raise preserves original traceback
raise ValueError(...) - Creates NEW exception, loses original traceback

Use bare raise when you want to log and continue propagating. Use raise NewException() from original when converting exception types.

Custom Exception Classes

Create custom exceptions by inheriting from Exception. This lets you add attributes, create exception hierarchies, and provide domain-specific error handling. Always inherit from Exception, not BaseException.

Output
Click Run to execute your code
Custom Exception Guidelines:
- Always inherit from Exception (not BaseException)
- Call super().__init__(message) in your __init__
- Add useful attributes (error codes, field names, etc.)
- Create a base exception for your app: class MyAppError(Exception)
- Specific exceptions inherit from base: class AuthError(MyAppError)

Exception Chaining

Exception chaining preserves the original error when wrapping it in a new exception. Use raise NewException() from original to explicitly chain. Use raise NewException() from None to suppress the chain (hide internal details).

Output
Click Run to execute your code
Chaining Summary:
raise X from Y - Explicit: "Y caused X" (__cause__)
raise X (during handling) - Implicit: "X happened while handling Y" (__context__)
raise X from None - Suppress chain (hide internal errors)

Use explicit chaining when converting internal errors to public API errors.

Common Mistakes

1. Raising wrong exception type

# Wrong - RuntimeError for validation
def set_age(age):
    if age < 0:
        raise RuntimeError("Age cannot be negative")  # Wrong type

# Correct - ValueError for invalid values
def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")  # Right type

# Wrong - using Exception (too generic)
raise Exception("Something went wrong")

# Correct - specific exception
raise ConnectionError("Failed to connect to database")

2. Losing traceback when re-raising

# Wrong - creates new exception, loses traceback!
try:
    process_data()
except ValueError as e:
    log_error(e)
    raise ValueError(str(e))  # NEW exception, lost traceback!

# Correct - bare raise preserves traceback
try:
    process_data()
except ValueError:
    log_error("Processing failed")
    raise  # Same exception, same traceback

# Also correct - explicit chaining preserves cause
try:
    process_data()
except ValueError as e:
    raise ProcessingError("Failed to process") from e

3. Forgetting to chain exceptions

# Wrong - original error lost
def get_config():
    try:
        return json.load(open("config.json"))
    except (FileNotFoundError, json.JSONDecodeError):
        raise ConfigError("Bad config")  # What was the original error?

# Correct - chain with 'from'
def get_config():
    try:
        return json.load(open("config.json"))
    except (FileNotFoundError, json.JSONDecodeError) as e:
        raise ConfigError("Bad config") from e  # Original preserved!

4. Inheriting from BaseException

# Wrong - BaseException isn't caught by 'except Exception'
class MyError(BaseException):  # Wrong!
    pass

try:
    raise MyError("oops")
except Exception:
    print("Not caught!")  # MyError escapes!

# Correct - inherit from Exception
class MyError(Exception):  # Correct!
    pass

try:
    raise MyError("oops")
except Exception:
    print("Caught!")  # Works!

5. Not calling super().__init__ in custom exception

# Wrong - forgets super().__init__
class APIError(Exception):
    def __init__(self, code, message):
        self.code = code
        self.message = message
        # Forgot super().__init__!

err = APIError(404, "Not found")
print(str(err))  # Empty string!

# Correct - call super().__init__
class APIError(Exception):
    def __init__(self, code, message):
        super().__init__(f"[{code}] {message}")
        self.code = code
        self.message = message

err = APIError(404, "Not found")
print(str(err))  # "[404] Not found"

Exercise: Validation Library

Task: Create a validation library with custom exceptions.

Requirements:

  • Create a base ValidationError exception
  • Create specific exceptions: RequiredFieldError, TypeValidationError, RangeError
  • Write a validate_user() function that validates user data
  • Chain exceptions properly to preserve context
Output
Click Run to execute your code
Show Solution
# Custom exceptions
class ValidationError(Exception):
    """Base validation error."""
    pass

class RequiredFieldError(ValidationError):
    """Field is required but missing."""
    def __init__(self, field):
        super().__init__(f"Field '{field}' is required")
        self.field = field

class TypeValidationError(ValidationError):
    """Field has wrong type."""
    def __init__(self, field, expected, got):
        super().__init__(f"Field '{field}' expected {expected}, got {got}")
        self.field = field
        self.expected = expected
        self.got = got

class RangeError(ValidationError):
    """Value out of allowed range."""
    def __init__(self, field, value, min_val=None, max_val=None):
        if min_val is not None and max_val is not None:
            msg = f"Field '{field}' value {value} not in range [{min_val}, {max_val}]"
        elif min_val is not None:
            msg = f"Field '{field}' value {value} must be >= {min_val}"
        else:
            msg = f"Field '{field}' value {value} must be <= {max_val}"
        super().__init__(msg)
        self.field = field
        self.value = value


def validate_user(data):
    """Validate user data dictionary."""
    # Check required fields
    for field in ['username', 'email', 'age']:
        if field not in data or data[field] is None:
            raise RequiredFieldError(field)

    # Validate types
    if not isinstance(data['username'], str):
        raise TypeValidationError('username', 'str', type(data['username']).__name__)

    if not isinstance(data['email'], str):
        raise TypeValidationError('email', 'str', type(data['email']).__name__)

    if not isinstance(data['age'], int):
        raise TypeValidationError('age', 'int', type(data['age']).__name__)

    # Validate ranges
    if len(data['username']) < 3:
        raise RangeError('username length', len(data['username']), min_val=3)

    if data['age'] < 0 or data['age'] > 150:
        raise RangeError('age', data['age'], min_val=0, max_val=150)

    if '@' not in data['email']:
        raise ValidationError("Email must contain '@'")

    return True


# Test the validation
test_users = [
    {},
    {'username': 'ab', 'email': '[email protected]', 'age': 25},
    {'username': 'alice', 'email': 'noatsign', 'age': 25},
    {'username': 'bob', 'email': '[email protected]', 'age': -5},
    {'username': 'charlie', 'email': '[email protected]', 'age': 30},
]

print("=== User Validation Tests ===\n")
for user in test_users:
    try:
        validate_user(user)
        print(f"Valid: {user}")
    except ValidationError as e:
        print(f"Invalid: {type(e).__name__}: {e}")

Summary

  • raise: raise ValueError("message") throws an exception
  • Choose right type: ValueError for bad values, TypeError for wrong types
  • Bare raise: raise re-raises current exception with traceback
  • Custom exceptions: Inherit from Exception, call super().__init__()
  • Exception hierarchy: Create base exception, specific ones inherit from it
  • raise from: raise X from Y chains exceptions (Y caused X)
  • from None: raise X from None suppresses chain
  • Add attributes: Custom exceptions can have extra data (code, field, etc.)

What's Next?

Now you can both catch and raise exceptions. But sometimes you just want to check that something is true during development - not handle it as an error, but catch bugs early. Next, we'll learn about assert statements for debugging and development-time checks!