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

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

.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;
            }
        }
    }
}

Комментарии (3) -

Александр 24.07.2019 17:30:15

А чем просто throw не угодил?

Антон 26.07.2019 1:05:15

Throw я как понимаю не подходит так как хочется развернуть исключение и выдать наружу Inner без потери стека?
Цель если честно тоже не совсем понял

Александр Просто throw не подойдет, если исключение было уже перехвачено и сохранено в InnerException. Вы throw выбросит повторно только текущее исключение.

Антон
Да, вы правильно поняли.

Ну в качестве примера - есть framework который выполняет команды, определяемые конкретным приложением. Он перехватывает исключения при выполнении команды, если надо делает retry, logging итд. В процессе для своих целей он может обернуть исходное исключение в свое.

В определенный момент может быть принято решение что исключение критическое и дальше пытаться продолжить выполнение команды смысла нет. Исключение надо выкинуть вызывающей стороне без внутренних оберток, исходного типа и с исходной информацией.

Добавить комментарий