Повторный выброс исключения без потери стека вызовов

.NET logo Иногда возникает ситуация, когда необходимо заново выбросить исключение ex , которое уже было выброшено, перехвачено и записано как внутренне ( InnerException ) в другом исключении. Проблема заключается в том, что если использовать вызов throw ex , то исходный стек вызова будет заменен на текущий. То есть потеряется важная для отладки информации. Но этого можно избежать.

Поддерживаемые платформы:

  • .NET 4.5 и выше
  • .NET Core 1.x и выше

ExceptionDispatchInfo

Методы и использование

Начиная с .NET 4.5 для решения описанной проблемы существует класс ExceptionDispatchInfo. Он позволяет повторно выбросить исключение, сохраненное в переменной, без потери исходного стека вызовов.

Публичный конструктор у данного класса отсутствует. Поэтому для создания его экземпляра необходимо передать исходное исключение в статический метод

public static ExceptionDispatchInfo ExceptionDispatchInfo.Capture (Exception source);

Полученный экземпляр может повторно выбрасывать исключение при помощи метода

public void Throw ();

При необходимости, исходное исключение можно прочитать в свойстве:

public Exception SourceException { get; }

В .NET Core 2.x и выше существует еще один статический метод, который сразу выполнить повторный выброс исключения:

public static ExceptionDispatchInfo.Throw(Exception source);

По факту он просто вызывает описанные выше методы: ExceptionDispatchInfo.Capture(source).Throw();

Принцип работы

ExceptionDispatchInfo при создании экземпляра сохраняет переданное исключение и его состояние, включая данные стека вызовов.

При вызове метода Throw() сначала в текущем потоке восстанавливается состояние, после чего следует вызов throw для исходного исключения.

Пример создаваемого стека вызовов

Посмотрим на стека вызовов, полученного при помощи ExceptionDispatchInfo:

   at ExceptionRethrow.Test.Save(String title) in Program.cs:line 29
   at ExceptionRethrow.Test.ProcessRequests() in Program.cs:line 35
--- End of stack trace from previous location where exception was thrown ---
   at ExceptionRethrow.Test.Dispatcher(Boolean useDispatchInfo) in Program.cs:line 49
   at ExceptionRethrow.Program.Main(String[] args) in Program.cs:line 13

Текст

--- End of stack trace from previous location where exception was thrown ---

используется как разделитель исходной (вверху) и текущей (внизу) цепочек вызовов. Пример кода, при помощи которого был получено данное сообщение, приведен ниже.

Пример кода

В коде для наглядности выделены стоки, на которые ссылается текст рассмотренного примера стека вызовов.

namespace ExceptionRethrow
{
    using System;
    using System.Runtime.ExceptionServices;

    internal class Program
    {
        static void Main(string[] args)
        {
            var test = new Test();

            try {
                test.Dispatch(useDispatchInfo: true);
            }
            catch (Exception ex) {
                Console.WriteLine(ex.GetType().FullName);
                Console.WriteLine(ex.StackTrace);
            }

            Console.ReadKey(intercept: true);
        }
    }

    public class Test
    {
        public void Save(string title)
        {
            if (title == null)
                throw new ArgumentNullException(nameof(title));
        }

        public void ProcessRequests()
        {
            try {
                this.Save(null);
            }
            catch (Exception ex) {
                throw new Exception("Wrapping source exception", ex);
            }
        }

        public void Dispatch(bool useDispatchInfo)
        {
            try {
                this.ProcessRequests();
            }
            catch (Exception ex) when (ex.InnerException != null) {
                if (useDispatchInfo)
                    ExceptionDispatchInfo.Capture(ex.InnerException).Throw();

                throw ex.InnerException;
            }
        }
    }
}