Поймать исключение, вызванное асинхронным методом

использование асинхронного CTP от Microsoft для .NET, можно ли поймать исключение, вызванное асинхронным методом в вызывающем методе?

public async void Foo()
{
    var x = await DoSomethingAsync();

    /* Handle the result, but sometimes an exception might be thrown.
       For example, DoSomethingAsync gets data from the network
       and the data is invalid... a ProtocolException might be thrown. */
}

public void DoFoo()
{
    try
    {
        Foo();
    }
    catch (ProtocolException ex)
    {
          /* The exception will never be caught.
             Instead when in debug mode, VS2010 will warn and continue.
             The deployed the app will simply crash. */
    }
}

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

6 ответов


это несколько странно читать, Но да, исключение будет пузыриться до вызывающего кода - но только если вы await или Wait() вызов Foo.

public async void DoFoo()
{
    try
    {
        await Foo();
    }
    catch (ProtocolException ex)
    {
          // The exception will be caught because you've awaited
          // the call in an async method.
    }
}

//or//

public void DoFoo()
{
    try
    {
        Foo().Wait();
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught because you've
             waited for the completion of the call. */
    }
} 

асинхронные методы void имеют различную семантику обработки ошибок. Когда исключение выбрасывается из асинхронной задачи или метода асинхронной задачи, это исключение захватывается и помещается в объект задачи. С методами async void нет объекта Task, поэтому любые исключения, выброшенные из асинхронного void метод будет вызываться непосредственно на SynchronizationContext, который был активен при запуске метода async void. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

обратите внимание, что использование Wait () может привести к блокировке приложения, если .Net решит выполнить ваш метод синхронно.

это объяснение http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions довольно хорошо-он обсуждает шаги компилятор принимает для достижения этой магии.


причина, по которой исключение не поймано, заключается в том, что метод Foo() имеет тип void return и поэтому, когда await вызывается, он просто возвращается. Поскольку DoFoo () не ожидает завершения Foo, обработчик исключений использовать нельзя.

Это открывает более простое решение, если вы можете изменить сигнатуру метода - изменить Foo() Так что он возвращает тип Task а то DoFoo() can await Foo(), как в этот код:

public async Task Foo() {
    var x = await DoSomethingThatThrows();
}

public async void DoFoo() {
    try {
        await Foo();
    } catch (ProtocolException ex) {
        // This will catch exceptions from DoSomethingThatThrows
    }
}

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

код ниже делает следующее:

  • создать 4 задачи
  • каждая задача будет асинхронно увеличивать число и возвращать увеличенное число
  • когда асинхронный результат прибыл, он прослеженный.

static TypeHashes _type = new TypeHashes(typeof(Program));        
private void Run()
{
    TracerConfig.Reset("debugoutput");

    using (Tracer t = new Tracer(_type, "Run"))
    {
        for (int i = 0; i < 4; i++)
        {
            DoSomeThingAsync(i);
        }
    }
    Application.Run();  // Start window message pump to prevent termination
}


private async void DoSomeThingAsync(int i)
{
    using (Tracer t = new Tracer(_type, "DoSomeThingAsync"))
    {
        t.Info("Hi in DoSomething {0}",i);
        try
        {
            int result = await Calculate(i);
            t.Info("Got async result: {0}", result);
        }
        catch (ArgumentException ex)
        {
            t.Error("Got argument exception: {0}", ex);
        }
    }
}

Task<int> Calculate(int i)
{
    var t = new Task<int>(() =>
    {
        using (Tracer t2 = new Tracer(_type, "Calculate"))
        {
            if( i % 2 == 0 )
                throw new ArgumentException(String.Format("Even argument {0}", i));
            return i++;
        }
    });
    t.Start();
    return t;
}

когда вы наблюдаете следы

22:25:12.649  02172/02820 {          AsyncTest.Program.Run 
22:25:12.656  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.657  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 0    
22:25:12.658  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.659  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.659  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 1    
22:25:12.660  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 2    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 3    
22:25:12.664  02172/02756          } AsyncTest.Program.Calculate Duration 4ms   
22:25:12.666  02172/02820          } AsyncTest.Program.Run Duration 17ms  ---- Run has completed. The async methods are now scheduled on different threads. 
22:25:12.667  02172/02756 Information AsyncTest.Program.DoSomeThingAsync Got async result: 1    
22:25:12.667  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 8ms    
22:25:12.667  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.665  02172/05220 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 0   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.668  02172/02756 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 2   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.724  02172/05220          } AsyncTest.Program.Calculate Duration 66ms      
22:25:12.724  02172/02756          } AsyncTest.Program.Calculate Duration 57ms      
22:25:12.725  02172/05220 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 0  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 106    
22:25:12.725  02172/02756 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 2  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 0      
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 70ms   
22:25:12.726  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   
22:25:12.726  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.726  02172/05220          } AsyncTest.Program.Calculate Duration 0ms   
22:25:12.726  02172/05220 Information AsyncTest.Program.DoSomeThingAsync Got async result: 3    
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   

вы заметите, что метод Run завершается в потоке 2820, в то время как только один дочерний поток завершен (2756). Если вы поместите try / catch вокруг своего метода await, вы можете "поймать" исключение обычным способом, хотя ваш код выполняется в другом потоке, когда задача вычисления завершена и выполняется ваша contiuation.

метод расчета следы исключение автоматически, потому что пользовалась ApiChange.Прикладной программный интерфейс.dll из ApiChange. Трассировка и отражатель очень помогают понять, что происходит. Чтобы избавиться от threading, вы можете создать свои собственные версии GetAwaiter BeginAwait и EndAwait и обернуть не задачу, а, например, ленивый и трассировку внутри собственных методов расширения. Тогда вы получите гораздо лучшее понимание того, что компилятор и что делает TPL.

теперь вы видите, что нет никакого способа, чтобы получить в try / catch ваше исключение назад, так как не осталось кадра стека для любого исключения для распространения. Ваш код может делать что-то совершенно другое после того, как вы инициировали асинхронные операции. Это может вызвать поток.Сон или даже прекращение. Пока есть один поток переднего плана, ваше приложение будет с радостью продолжать выполнять асинхронные задачи.


вы можете обработать исключение внутри асинхронного метода после завершения асинхронной операции и перезвоните в поток пользовательского интерфейса. Рекомендуемый способ сделать это с TaskScheduler.FromSynchronizationContext. Это работает только если у вас есть поток пользовательского интерфейса и он не очень занят другими делами.


также важно отметить, что вы потеряете хронологическую трассировку стека исключения, если у вас есть тип возврата void для асинхронного метода. Я бы рекомендовал вернуть задачу следующим образом. Это значительно облегчит отладку.

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }

исключение может быть поймано в функции async.

public async void Foo()
{
    try
    {
        var x = await DoSomethingAsync();
        /* Handle the result, but sometimes an exception might be thrown
           For example, DoSomethingAsync get's data from the network
           and the data is invalid... a ProtocolException might be thrown */
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught here */
    }
}

public void DoFoo()
{
    Foo();
}

этот блог аккуратно объясняет вашу проблему Ссылался.

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

рекомендуется изменить тип возврата на Task. Кроме того, попробуйте кодировать асинхронный весь путь, сделать каждый вызов асинхронного метода и вызываться из асинхронных методов. Кроме основного метода в консоли, которая не может быть асинхронной (до C# 7.1).

вы столкнетесь с тупиками с GUI и ASP.NET приложения, если вы игнорируете эту передовую практику. Взаимоблокировка происходит потому, что эти приложения выполняются в контексте, который позволяет только один поток и не будет отказываться от него асинхронного потока. Это означает, что GUI синхронно ожидает возврата, в то время как асинхронный метод ожидает контекст: взаимоблокировка.

Это поведение не будет происходить в консольном приложении, потому что это выполняется в контексте с пулом потоков. Метод async вернется в другой поток,который будет запланирован. Вот почему приложение тестовой консоли будет работать, но те же вызовы будут взаимоблокировки в других приложениях...