[C#] Correctly Using Exception Re-throwing (throw;) and Wrapping (InnerException)

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:

  1. Re-throw as is (throw;): After performing tasks like logging, the exception is thrown up to the higher level exactly as it occurred.
  2. Wrap and throw (throw new ...): The occurred exception is stored in the InnerException property, 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 (the LoadImageFile method). 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.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次