Рекомендации по перехвату и повторному выбрасыванию исключений .NET

Какие передовые практики следует учитывать при перехвате исключений и их повторном выбросе? Я хочу убедиться, что объект Exception InnerException и трассировка стека сохранены. Есть ли разница между следующими блоками кода в том, как они справляются с этим?

try
{
    //some code
}
catch (Exception ex)
{
    throw ex;
}

Vs:

try
{
    //some code
}
catch
{
    throw;
}

person Seibar    schedule 22.08.2008    source источник


Ответы (11)


Способ сохранить трассировку стека - использовать throw; Это также верно

try {
  // something that bombs here
} catch (Exception ex)
{
    throw;
}

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

Майк также верен, если исключение позволяет передать исключение (которое Рекомендовано).

Карл Сегин имеет отличная статья об обработке исключений в его электронная книга по основам программирования, которую стоит прочитать.

Изменить: рабочая ссылка на Основы программирования pdf. Просто найдите в тексте «исключение».

person Darren Kopp    schedule 22.08.2008
comment
Я не уверен, что эта статья прекрасна, она предлагает попробовать {// ...} catch (Exception ex) {throw new Exception (ex.Message + другое); } это хорошо. Проблема в том, что вы совершенно не можете обрабатывать это исключение дальше по стеку, если вы не поймаете все исключения, большое «нет-нет» (вы уверены, что хотите обработать это OutOfMemoryException?) - person ljs; 22.06.2009
comment
@ljs Изменилась ли статья после вашего комментария, поскольку я не вижу ни одного раздела, где он рекомендует это. На самом деле, как раз наоборот, он говорит не делать этого и спрашивает, хотите ли вы также обработать OutOfMemoryException !? - person RyanfaeScotland; 31.03.2015
comment
Иногда throw; недостаточно для сохранения трассировки стека. Вот пример https://dotnetfiddle.net/CkMFoX - person Artavazd Balayan; 07.10.2016
comment
Или ExceptionDispatchInfo.Capture(ex).Throw(); throw; в .NET +4.5 stackoverflow.com/questions/57383/ - person Alfred Wallace; 12.04.2019

Если вы создадите новое исключение с начальным исключением, вы также сохраните начальную трассировку стека.

try{
} 
catch(Exception ex){
     throw new MoreDescriptiveException("here is what was happening", ex);
}
person Mike    schedule 22.08.2008
comment
Независимо от того, что я пытаюсь выбросить, новое исключение (сообщение, ex) всегда выбрасывает ex и игнорирует настраиваемое сообщение. throw new Exception (message, ex.InnerException) работает. - person Tod; 02.04.2015
comment
Если пользовательское исключение не требуется, можно использовать AggregateException (.NET 4+) msdn.microsoft.com/en-us/library/ - person Nikos Tsokos; 11.11.2015
comment
AggregateException следует использовать только для исключений из агрегированных операций. Например, он создается классами ParallelEnumerable и Task среды CLR. Использование, вероятно, должно следовать этому примеру. - person Aluan Haddad; 27.02.2017

На самом деле, есть некоторые ситуации, в которых throw статус не сохраняет информацию StackTrace. Например, в приведенном ниже коде:

try
{
  int i = 0;
  int j = 12 / i; // Line 47
  int k = j + 1;
}
catch
{
  // do something
  // ...
  throw; // Line 54
}

StackTrace укажет, что в строке 54 возникло исключение, хотя оно возникло в строке 47.

Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
   at Program.WithThrowIncomplete() in Program.cs:line 54
   at Program.Main(String[] args) in Program.cs:line 106

В ситуациях, подобных описанной выше, есть два варианта сохранить исходный StackTrace:

Вызов Exception.InternalPreserveStackTrace

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

private static void PreserveStackTrace(Exception exception)
{
  MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
    BindingFlags.Instance | BindingFlags.NonPublic);
  preserveStackTrace.Invoke(exception, null);
}

У меня есть недостаток в том, что я полагаюсь на частный метод для сохранения информации StackTrace. Он может быть изменен в будущих версиях .NET Framework. Пример кода выше и предлагаемое ниже решение были извлечены из Журнал Fabrice MARGUERIE.

Вызов Exception.SetObjectData

Приведенный ниже метод был предложен Антоном Тихи в качестве ответа на В C #, как я могу повторно выбросить InnerException без потери трассировки стека вопрос.

static void PreserveStackTrace (Exception e) 
{ 
  var ctx = new StreamingContext  (StreamingContextStates.CrossAppDomain) ; 
  var mgr = new ObjectManager     (null, ctx) ; 
  var si  = new SerializationInfo (e.GetType (), new FormatterConverter ()) ; 

  e.GetObjectData    (si, ctx)  ; 
  mgr.RegisterObject (e, 1, si) ; // prepare for SetObjectData 
  mgr.DoFixups       ()         ; // ObjectManager calls SetObjectData 

  // voila, e is unmodified save for _remoteStackTraceString 
} 

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

protected Exception(
    SerializationInfo info,
    StreamingContext context
)

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

person CARLOS LOTH    schedule 01.07.2012
comment
Вы можете перехватить исключение и опубликовать это исключение где угодно. Затем киньте новый, объясняющий, что случилось с пользователем. Таким образом, вы можете увидеть, что произошло в текущий момент, когда исключение было перехвачено, и пользователь может не заботиться о том, что было фактическим исключением. - person zaggler; 17.11.2012
comment
В .NET 4.5 есть третий и, на мой взгляд, более чистый вариант: используйте ExceptionDispatchInfo. См. Ответ Tragedians на связанный вопрос здесь: stackoverflow.com/a/17091351/567000 для получения дополнительной информации. - person Søren Boisen; 30.12.2015
comment
То, что простой throw; здесь показывает строку 54 вместо строки 47, вероятно, следует рассматривать как давнюю ошибку в .NET Framework, которая была исправлена ​​в .NET Core 2.1 (github.com/dotnet/runtime/issues/9518). Вы можете использовать ExceptionDispatchInfo, но это не один из его основных вариантов использования (один из которых показан в stackoverflow.com/a/17091351), и предположение, что это запутывает воду и приводит к менее читаемому коду. Тем не менее, наличие строки из улова и любых дополнительных номеров строк из стека вызовов всегда было для меня достаточно хорошим. - person Johann; 09.09.2020

Когда вы throw ex, вы по существу генерируете новое исключение и пропускаете исходную информацию трассировки стека. throw - предпочтительный метод.

person Forgotten Semicolon    schedule 22.08.2008

Практическое правило - избегать ловли и бросания базового Exception объекта. Это заставляет вас быть немного умнее в отношении исключений; другими словами, у вас должен быть явный улов для SqlException, чтобы ваш код обработки не сделал что-то неправильное с NullReferenceException.

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

person swilliams    schedule 22.08.2008
comment
Я думаю, что для ведения журнала лучше всего иметь дело с необработанными исключениями с помощью исключений AppDomain.CurrentDomain.UnhandledException и Application.ThreadException. Использование повсюду больших блоков try {...} catch (Exception ex) {...} означает большое количество дублирования. Зависит от того, хотите ли вы регистрировать обработанные исключения, и в этом случае (хотя бы минимальное) дублирование может быть неизбежным. - person ljs; 22.06.2009
comment
Кроме того, использование этих событий означает, что вы делаете журнал всех необработанных исключений, тогда как если вы используете большие старые блоки try {...} catch (Exception ex) {...}, вы можете пропустить некоторые из них. - person ljs; 22.06.2009

Никто не объяснил разницу между ExceptionDispatchInfo.Capture( ex ).Throw() и простым throw, так что вот оно. Однако некоторые люди заметили проблему с throw.

Полный способ повторно вызвать перехваченное исключение - использовать ExceptionDispatchInfo.Capture( ex ).Throw() (доступно только в .Net 4.5).

Ниже приведены примеры, необходимые для проверки:

1.

void CallingMethod()
{
    //try
    {
        throw new Exception( "TEST" );
    }
    //catch
    {
    //    throw;
    }
}

2.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        ExceptionDispatchInfo.Capture( ex ).Throw();
        throw; // So the compiler doesn't complain about methods which don't either return or throw.
    }
}

3.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch
    {
        throw;
    }
}

4.

void CallingMethod()
{
    try
    {
        throw new Exception( "TEST" );
    }
    catch( Exception ex )
    {
        throw new Exception( "RETHROW", ex );
    }
}

Случай 1 и случай 2 предоставят вам трассировку стека, где номер строки исходного кода для метода CallingMethod - это номер строки строки throw new Exception( "TEST" ).

Однако вариант 3 предоставит вам трассировку стека, где номер строки исходного кода для метода CallingMethod является номером строки вызова throw. Это означает, что если строка throw new Exception( "TEST" ) окружена другими операциями, вы не знаете, на каком номере строки фактически было сгенерировано исключение.

Случай 4 аналогичен случаю 2, потому что номер строки исходного исключения сохраняется, но не является реальным повторным вызовом, поскольку он изменяет тип исходного исключения.

person jeuoekdcwzfwccu    schedule 14.11.2016
comment
Добавьте простое объявление, чтобы никогда не использовать throw ex;, и это лучший ответ из всех. - person NH.; 05.04.2018

Вы всегда должны использовать "throw;" чтобы повторно выбросить исключения в .NET,

Обратитесь к этому, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

В основном MSIL (CIL) имеет две инструкции - «выбросить» и «перебросить»:

  • C # "throw ex;" компилируется в "throw" MSIL
  • C # "throw;" - в МСИЛ "перебросить"!

В принципе, я могу понять причину, по которой "throw ex" переопределяет трассировку стека.

person Vinod T. Patil    schedule 05.07.2010
comment
Ссылка - ну, на самом деле источник, на который ссылается ссылка - заполнена хорошей информации, а также отмечает возможного виновника того, почему многие думают, что throw ex; будет перебрасывать - в Java это так! Но вы должны включить эту информацию здесь, чтобы получить ответ класса А. (Хотя я все еще догоняю ExceptionDispatchInfo.Capture ответ от jeuoekdcwzfwccu.) - person ruffin; 13.01.2020

Некоторые люди действительно упустили очень важный момент - 'throw' и 'throw ex' могут делать то же самое, но они не дают вам важной информации, которая является строкой, в которой произошло исключение.

Рассмотрим следующий код:

static void Main(string[] args)
{
    try
    {
        TestMe();
    }
    catch (Exception ex)
    {
        string ss = ex.ToString();
    }
}

static void TestMe()
{
    try
    {
        //here's some code that will generate an exception - line #17
    }
    catch (Exception ex)
    {
        //throw new ApplicationException(ex.ToString());
        throw ex; // line# 22
    }
}

Когда вы выполняете либо 'throw', либо 'throw ex', вы получаете трассировку стека, но строка # будет # 22, поэтому вы не можете определить, какая именно строка выбрасывала исключение (если у вас только 1 или несколько строк кода в блоке try). Чтобы получить ожидаемую строку № 17 в вашем исключении, вам нужно будет создать новое исключение с исходной трассировкой стека исключений.

person notlkk    schedule 08.02.2012
comment
То, что простой throw; здесь также показывает строку 22 вместо строки 17, вероятно, следует рассматривать как давнюю ошибку в .NET Framework и было исправлено в .NET Core 2.1 (github.com/dotnet/runtime/issues/9518). Тем не менее, я никогда не видел случая, когда мне хотелось бы увидеть строку из try в трассировке стека. Наличие строки из улова и любых дополнительных номеров строк из стека вызовов всегда было достаточно. - person Johann; 09.09.2020

Вы также можете использовать:

try
{
// Dangerous code
}
finally
{
// clean up, or do nothing
}

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

person Erick B    schedule 22.08.2008

Я бы определенно использовал:

try
{
    //some code
}
catch
{
    //you should totally do something here, but feel free to rethrow
    //if you need to send the exception up the stack.
    throw;
}

Это сохранит ваш стек.

person 1kevgriff    schedule 22.08.2008
comment
Честно говоря, в 2008 году OP спрашивал, как сохранить стек, и в 2008 году я дал правильный ответ. Чего не хватает в моем ответе, так это того, что действительно нужно что-то делать. - person 1kevgriff; 30.04.2015
comment
@JohnSaunders Это верно тогда и только тогда, когда вы ничего не делаете до throw; например, вы можете очистить одноразовый предмет (где вы вызываете его ТОЛЬКО при ошибке), а затем выбросите исключение. - person Meirion Hughes; 21.04.2016
comment
@meirion, когда я написал комментарий, до броска ничего не было. Когда это было добавлено, я проголосовал за, но не удалил комментарий. - person John Saunders; 21.04.2016

К вашему сведению, я только что проверил это и трассировку стека, о которой сообщает 'throw;' не совсем корректная трассировка стека. Пример:

    private void foo()
    {
        try
        {
            bar(3);
            bar(2);
            bar(1);
            bar(0);
        }
        catch(DivideByZeroException)
        {
            //log message and rethrow...
            throw;
        }
    }

    private void bar(int b)
    {
        int a = 1;
        int c = a/b;  // Generate divide by zero exception.
    }

Трассировка стека правильно указывает на источник исключения (указанный номер строки), но номер строки, сообщаемый для foo (), является строкой выброса; оператор, поэтому вы не можете сказать, какой из вызовов bar () вызвал исключение.

person redcalx    schedule 13.04.2011
comment
Вот почему лучше не пытаться перехватывать исключения, если вы не планируете что-то с ними делать. - person Nate Zaugg; 11.10.2011