In synchronous code, exceptions work their way up the call stack, back
through each method call until they reach either a
that can catch them, or they leave your code. In async code, particularly
after a method has been resumed after an
await, the current call stack has very little to
do with the programmer’s intention, and mostly contains framework logic to
resume the async method. The exception would be impossible to catch in your
calling code, and stack traces wouldn’t be very helpful at all, so C#
changes the behavior of exceptions to be more useful.
You can still see the raw call stack in a debugger.
Most async methods you write will return
of these methods, each awaiting the next, are the asynchronous version of
the call stack we’re familiar with in synchronous code. C# strives to make
the behavior of exceptions in these methods feel very similar to working
with synchronous methods. In particular,
blocks placed around an awaited async method will catch exceptions thrown
inside that async method.
// Execution will reach here
Until execution reaches the first
await, the synchronous call stack and the chain of async methods are exactly the same. The behavior of exceptions at that point is still changed ...