In layered application architectures, there are frequently situations where you need to propagate exceptions occurring in lower-level processing to higher-level callers.
In such cases, simply passing the exception from right to left is not enough. By appropriately “re-throwing” or “wrapping” exceptions, you can significantly improve debuggability and code maintainability.
This article explains how to re-throw exceptions while maintaining the stack trace and how to convert low-level exceptions into business logic exceptions, using a user profile update process as an example.
Exception Propagation Patterns
There are two main approaches to re-throwing exceptions in exception handling:
- Re-throw as is (
throw;): After performing tasks like logging, the exception is thrown up to the higher level exactly as it occurred. - Wrap and throw (
throw new ...): The occurred exception is stored in theInnerExceptionproperty, and a different, more abstract exception (such as a business exception) is thrown instead.
Practical Code Example: Profile Update and Image Processing
The following code demonstrates the coordination between a low-level process (loading a user avatar image) and a high-level process (updating the profile) that calls it.
using System;
using System.IO;
namespace UserProfileSystem
{
// Custom business exception class
public class UserUpdateException : Exception
{
// Define a constructor that accepts InnerException
public UserUpdateException(string message, Exception innerException)
: base(message, innerException)
{
}
}
class Program
{
static void Main()
{
try
{
// Call higher-level processing
UpdateUserAvatar("user_001", "missing_image.png");
}
catch (UserUpdateException ex)
{
// Pattern 1: Catching wrapped exceptions
Console.WriteLine("=== Business Error Occurred ===");
Console.WriteLine($"Message: {ex.Message}");
// Check the original cause (InnerException)
if (ex.InnerException != null)
{
Console.WriteLine($"Root Cause: {ex.InnerException.GetType().Name}");
Console.WriteLine($"Details: {ex.InnerException.Message}");
}
}
catch (Exception ex)
{
// Pattern 2: Catching re-thrown exceptions
Console.WriteLine("=== Unexpected System Error ===");
Console.WriteLine($"Message: {ex.Message}");
// Verify that the stack trace is maintained
Console.WriteLine($"Stack Trace:\n{ex.StackTrace}");
}
}
static void UpdateUserAvatar(string userId, string imagePath)
{
try
{
// Execute image loading process
LoadImageFile(imagePath);
}
catch (FileNotFoundException ex)
{
// [Pattern 1: Wrapping the Exception]
// Convert the fact "file missing" into the business meaning "update failed".
// By passing the original exception (ex) as the second argument, it is held as InnerException.
throw new UserUpdateException($"Failed to update avatar for ID: {userId}.", ex);
}
catch (UnauthorizedAccessException)
{
// [Pattern 2: Re-throwing the Exception]
// Only perform logging, leave the exception handling to the upper level.
Console.WriteLine($"[Log] Access permission error occurred. Path: {imagePath}");
// IMPORTANT: Use "throw;" NOT "throw ex;".
throw;
}
}
// Simulation of low-level file operation
static void LoadImageFile(string path)
{
// Forcibly generate exceptions for explanation
if (path.Contains("missing"))
{
throw new FileNotFoundException("The specified image file was not found.", path);
}
if (path.Contains("protected"))
{
throw new UnauthorizedAccessException("Access to the file was denied.");
}
}
}
}
Execution Result
=== Business Error Occurred ===
Message: Failed to update avatar for ID: user_001.
Root Cause: FileNotFoundException
Details: The specified image file was not found.
Technical Points and Precautions
1. The Critical Difference Between throw; and throw ex;
When re-throwing an exception as is, you must use throw;.
throw;(Recommended): Maintains the stack trace information from the original location where the exception occurred (theLoadImageFilemethod). This allows you to identify the “true source” during debugging.throw ex;(Prohibited): Resets the stack trace. The location of the exception is rewritten to the “line where it was re-thrown (inside the catch block),” making it impossible to know the original source.
2. Preserving Information with InnerException
When converting (wrapping) an exception into another exception, always design your code to pass the original exception to the constructor (usually the second argument). This allows the upper layers to handle “business logic errors” while still enabling the logging of “technical root causes” by tracing the InnerException.
3. Unifying Abstraction Levels
Generally, exceptions thrown by a method should match the method’s level of abstraction. For example, rather than a “User Update Method” throwing internal implementation details like “SQL Error” or “File Not Found Error” directly, it is better to wrap them in a dedicated exception like UserUpdateException. This keeps the caller code (such as the UI layer) simple and preserves encapsulation.
Summary
In implementing exception handling, “re-throwing” is a vital means of controlling the preservation and concealment of information.
- Use
throw;to pass the exception up while preserving information. - Use
throw new XxxException(msg, inner)to convert the meaning before passing it up.
By properly distinguishing between these two, you can build robust and maintainable error handling.
