Re-raising Caught Exceptions Patterns
๐ท๏ธ Error Handling and Exceptions / Raising Exceptions
When handling exceptions in Python, there are times when you need to catch an error, perform some action (like logging or cleanup), and then let the exception continue propagating up the call stack. This is known as re-raising. It allows you to acknowledge an error occurred, do something about it, and still let the program's higher-level error handlers deal with it appropriately.
โ๏ธ Why Re-raise Exceptions?
Re-raising is useful when you want to:
- Log the error at a specific point in your code, but still let the caller handle it.
- Perform cleanup (e.g., closing a file or releasing a network connection) before the exception moves up.
- Add context to an exception without suppressing it.
- Filter exceptions โ catch only certain conditions, then re-raise others.
The key idea is that you catch the exception not to fully handle it, but to observe or augment it before letting it continue.
๐ ๏ธ Basic Re-raising with raise (No Argument)
The simplest pattern is to use a bare raise statement inside an except block. This re-raises the same exception that was caught, preserving its original traceback.
- Use a try block to wrap code that might fail.
- In the except block, perform your action (like logging).
- Then call raise with no argument to re-raise the exact same exception.
Example flow:
- You attempt to open a file that does not exist.
- The FileNotFoundError is caught.
- You log a message like "File not found, re-raising."
- The bare raise sends the same error upward.
- The program stops with the original traceback, plus your log message.
This pattern is ideal when you want to record that an error happened but not change how the rest of the program sees it.
๐ต๏ธ Re-raising with a Different Exception
Sometimes you want to catch one type of exception and raise a different, more specific exception in its place. This is common when building libraries or APIs where you want to hide implementation details.
- Catch the original exception (e.g., ValueError).
- Raise a new exception (e.g., CustomAPIError) with a meaningful message.
- You can optionally pass the original exception as the cause using the from keyword.
Using from links the new exception to the original, preserving the full traceback chain. This is very helpful for debugging.
Example flow:
- Your function tries to parse user input as an integer.
- A ValueError occurs because the input is invalid.
- You catch it and raise a CustomValidationError with a message like "Invalid user ID format."
- Using from ensures the original ValueError is still visible in the traceback.
This pattern is useful for abstraction โ the caller only sees your custom exception, but you can still trace back to the root cause.
๐ Comparison: Bare raise vs. Raising a New Exception
| Feature | Bare raise (No Argument) |
Raising a New Exception |
|---|---|---|
| What happens | Re-raises the exact same exception object | Creates and raises a different exception |
| Traceback | Original traceback is fully preserved | Original traceback is preserved if using from |
| Use case | Logging, cleanup, monitoring | Abstraction, adding context, custom error types |
| Exception type | Unchanged | Changed to a new type |
| Message | Original message stays | You provide a new message |
๐งฐ Common Re-raising Patterns
Pattern 1: Log and Re-raise
- Catch the exception.
- Write a log entry with details about the error.
- Use bare raise to let the exception continue.
This is the most common pattern in production systems where you need an audit trail of failures.
Pattern 2: Cleanup and Re-raise
- Catch the exception.
- Perform necessary cleanup (close files, release locks, restore state).
- Use bare raise to propagate the error.
This ensures resources are freed even when an error occurs, without swallowing the exception.
Pattern 3: Transform and Re-raise
- Catch a low-level exception.
- Raise a higher-level custom exception with a clearer message.
- Use from to link back to the original error.
This is standard practice in library design to shield users from internal details.
Pattern 4: Conditional Re-raise
- Catch a broad exception type.
- Inspect the exception (e.g., check its attributes or message).
- If it matches certain criteria, handle it locally.
- Otherwise, re-raise it with bare raise.
This allows you to selectively handle only specific error conditions while letting others pass through.
โ ๏ธ Important Considerations
- Never use bare
raiseoutside an except block โ it will raise a RuntimeError with no context. - Always use
fromwhen raising a new exception to preserve the original traceback. Without it, debugging becomes much harder. - Avoid silent re-raises โ if you catch an exception and immediately re-raise without any action, the try-except is unnecessary.
- Be careful with cleanup code โ if your cleanup itself raises an exception, it will override the original error. Use a finally block or handle cleanup carefully.
๐งช Practical Advice for Engineers
- Start with the log and re-raise pattern โ it is simple and adds immediate value.
- Use transform and re-raise when building reusable functions or modules that others will call.
- Always test your re-raise logic to ensure the traceback is informative and complete.
- Remember that re-raising is about observation and delegation, not about hiding errors.
Re-raising is a powerful tool that gives you fine-grained control over error handling while maintaining the integrity of the exception chain. Used correctly, it makes your code more robust, debuggable, and maintainable.
Re-raising caught exceptions allows an engineer to catch an error, perform some action, and then let the same exception continue propagating up the call stack.
๐ข Example 1: Basic re-raise with raise (no argument)
This example catches a ZeroDivisionError, prints a message, and re-raises the same exception.
try:
result = 10 / 0
except ZeroDivisionError:
print("Caught division by zero โ re-raising now")
raise
๐ค Output: Caught division by zero โ re-raising now
(then the program crashes with ZeroDivisionError)
๐ก Example 2: Re-raise the same exception object explicitly
This example stores the caught exception in a variable and re-raises it using raise exc.
try:
value = int("not_a_number")
except ValueError as exc:
print(f"Caught ValueError: {exc}")
raise exc
๐ค Output: Caught ValueError: invalid literal for int() with base 10: 'not_a_number'
(then the program crashes with ValueError)
๐ Example 3: Log the error and re-raise
This example logs the error message to the console and then re-raises the exception for upstream handling.
import logging
logging.basicConfig(level=logging.ERROR)
try:
numbers = [1, 2, 3]
print(numbers[10])
except IndexError as exc:
logging.error(f"Index out of range: {exc}")
raise
๐ค Output: ERROR:root:Index out of range: list index out of range
(then the program crashes with IndexError)
๐ต Example 4: Perform cleanup before re-raising
This example closes a simulated file resource before re-raising the exception.
file_handle = "open_file.txt"
try:
print(f"Working with {file_handle}")
raise RuntimeError("Disk full")
except RuntimeError as exc:
print(f"Closing {file_handle} before re-raising")
file_handle = None
raise
๐ค Output: Working with open_file.txt
Closing open_file.txt before re-raising
(then the program crashes with RuntimeError: Disk full)
๐ฃ Example 5: Re-raise with additional context using a new exception
This example catches a KeyError, wraps it with a more descriptive ValueError, and re-raises the new exception.
config = {"host": "localhost"}
try:
port = config["port"]
except KeyError as exc:
raise ValueError(f"Missing required config key: {exc}") from exc
๐ค Output: ValueError: Missing required config key: 'port'
(the original KeyError is shown as the cause)
Comparison Table: Re-raise Patterns
| Pattern | Use Case | Preserves Original Traceback |
|---|---|---|
raise (no argument) |
Simple re-raise after logging or cleanup | โ Yes |
raise exc |
Explicit re-raise of stored exception | โ Yes |
raise NewException from exc |
Wrap exception with additional context | โ Yes (chained) |
raise inside except block |
Default re-raise behavior | โ Yes |
raise NewException (no from) |
Replace exception entirely | โ No (original lost) |
When handling exceptions in Python, there are times when you need to catch an error, perform some action (like logging or cleanup), and then let the exception continue propagating up the call stack. This is known as re-raising. It allows you to acknowledge an error occurred, do something about it, and still let the program's higher-level error handlers deal with it appropriately.
โ๏ธ Why Re-raise Exceptions?
Re-raising is useful when you want to:
- Log the error at a specific point in your code, but still let the caller handle it.
- Perform cleanup (e.g., closing a file or releasing a network connection) before the exception moves up.
- Add context to an exception without suppressing it.
- Filter exceptions โ catch only certain conditions, then re-raise others.
The key idea is that you catch the exception not to fully handle it, but to observe or augment it before letting it continue.
๐ ๏ธ Basic Re-raising with raise (No Argument)
The simplest pattern is to use a bare raise statement inside an except block. This re-raises the same exception that was caught, preserving its original traceback.
- Use a try block to wrap code that might fail.
- In the except block, perform your action (like logging).
- Then call raise with no argument to re-raise the exact same exception.
Example flow:
- You attempt to open a file that does not exist.
- The FileNotFoundError is caught.
- You log a message like "File not found, re-raising."
- The bare raise sends the same error upward.
- The program stops with the original traceback, plus your log message.
This pattern is ideal when you want to record that an error happened but not change how the rest of the program sees it.
๐ต๏ธ Re-raising with a Different Exception
Sometimes you want to catch one type of exception and raise a different, more specific exception in its place. This is common when building libraries or APIs where you want to hide implementation details.
- Catch the original exception (e.g., ValueError).
- Raise a new exception (e.g., CustomAPIError) with a meaningful message.
- You can optionally pass the original exception as the cause using the from keyword.
Using from links the new exception to the original, preserving the full traceback chain. This is very helpful for debugging.
Example flow:
- Your function tries to parse user input as an integer.
- A ValueError occurs because the input is invalid.
- You catch it and raise a CustomValidationError with a message like "Invalid user ID format."
- Using from ensures the original ValueError is still visible in the traceback.
This pattern is useful for abstraction โ the caller only sees your custom exception, but you can still trace back to the root cause.
๐ Comparison: Bare raise vs. Raising a New Exception
| Feature | Bare raise (No Argument) |
Raising a New Exception |
|---|---|---|
| What happens | Re-raises the exact same exception object | Creates and raises a different exception |
| Traceback | Original traceback is fully preserved | Original traceback is preserved if using from |
| Use case | Logging, cleanup, monitoring | Abstraction, adding context, custom error types |
| Exception type | Unchanged | Changed to a new type |
| Message | Original message stays | You provide a new message |
๐งฐ Common Re-raising Patterns
Pattern 1: Log and Re-raise
- Catch the exception.
- Write a log entry with details about the error.
- Use bare raise to let the exception continue.
This is the most common pattern in production systems where you need an audit trail of failures.
Pattern 2: Cleanup and Re-raise
- Catch the exception.
- Perform necessary cleanup (close files, release locks, restore state).
- Use bare raise to propagate the error.
This ensures resources are freed even when an error occurs, without swallowing the exception.
Pattern 3: Transform and Re-raise
- Catch a low-level exception.
- Raise a higher-level custom exception with a clearer message.
- Use from to link back to the original error.
This is standard practice in library design to shield users from internal details.
Pattern 4: Conditional Re-raise
- Catch a broad exception type.
- Inspect the exception (e.g., check its attributes or message).
- If it matches certain criteria, handle it locally.
- Otherwise, re-raise it with bare raise.
This allows you to selectively handle only specific error conditions while letting others pass through.
โ ๏ธ Important Considerations
- Never use bare
raiseoutside an except block โ it will raise a RuntimeError with no context. - Always use
fromwhen raising a new exception to preserve the original traceback. Without it, debugging becomes much harder. - Avoid silent re-raises โ if you catch an exception and immediately re-raise without any action, the try-except is unnecessary.
- Be careful with cleanup code โ if your cleanup itself raises an exception, it will override the original error. Use a finally block or handle cleanup carefully.
๐งช Practical Advice for Engineers
- Start with the log and re-raise pattern โ it is simple and adds immediate value.
- Use transform and re-raise when building reusable functions or modules that others will call.
- Always test your re-raise logic to ensure the traceback is informative and complete.
- Remember that re-raising is about observation and delegation, not about hiding errors.
Re-raising is a powerful tool that gives you fine-grained control over error handling while maintaining the integrity of the exception chain. Used correctly, it makes your code more robust, debuggable, and maintainable.
Interactive Views
You are currently in ๐ All-in-One mode. Use the tabs at the top to switch to ๐ Theory Only or ๐ป Code Only views.
Re-raising caught exceptions allows an engineer to catch an error, perform some action, and then let the same exception continue propagating up the call stack.
๐ข Example 1: Basic re-raise with raise (no argument)
This example catches a ZeroDivisionError, prints a message, and re-raises the same exception.
try:
result = 10 / 0
except ZeroDivisionError:
print("Caught division by zero โ re-raising now")
raise
๐ค Output: Caught division by zero โ re-raising now
(then the program crashes with ZeroDivisionError)
๐ก Example 2: Re-raise the same exception object explicitly
This example stores the caught exception in a variable and re-raises it using raise exc.
try:
value = int("not_a_number")
except ValueError as exc:
print(f"Caught ValueError: {exc}")
raise exc
๐ค Output: Caught ValueError: invalid literal for int() with base 10: 'not_a_number'
(then the program crashes with ValueError)
๐ Example 3: Log the error and re-raise
This example logs the error message to the console and then re-raises the exception for upstream handling.
import logging
logging.basicConfig(level=logging.ERROR)
try:
numbers = [1, 2, 3]
print(numbers[10])
except IndexError as exc:
logging.error(f"Index out of range: {exc}")
raise
๐ค Output: ERROR:root:Index out of range: list index out of range
(then the program crashes with IndexError)
๐ต Example 4: Perform cleanup before re-raising
This example closes a simulated file resource before re-raising the exception.
file_handle = "open_file.txt"
try:
print(f"Working with {file_handle}")
raise RuntimeError("Disk full")
except RuntimeError as exc:
print(f"Closing {file_handle} before re-raising")
file_handle = None
raise
๐ค Output: Working with open_file.txt
Closing open_file.txt before re-raising
(then the program crashes with RuntimeError: Disk full)
๐ฃ Example 5: Re-raise with additional context using a new exception
This example catches a KeyError, wraps it with a more descriptive ValueError, and re-raises the new exception.
config = {"host": "localhost"}
try:
port = config["port"]
except KeyError as exc:
raise ValueError(f"Missing required config key: {exc}") from exc
๐ค Output: ValueError: Missing required config key: 'port'
(the original KeyError is shown as the cause)
Comparison Table: Re-raise Patterns
| Pattern | Use Case | Preserves Original Traceback |
|---|---|---|
raise (no argument) |
Simple re-raise after logging or cleanup | โ Yes |
raise exc |
Explicit re-raise of stored exception | โ Yes |
raise NewException from exc |
Wrap exception with additional context | โ Yes (chained) |
raise inside except block |
Default re-raise behavior | โ Yes |
raise NewException (no from) |
Replace exception entirely | โ No (original lost) |