An exception from an async call never reaches the catch
A handler saves an order and is expected to surface any failure to its caller. SaveAsync throws a DbException when the database rejects the write, yet the catch below never runs, nothing is logged, and the caller sees a successful completion.
Constraints: do not change SaveAsync; keep HandleAsync asynchronous; the caller must end up observing the failure.
public async Task HandleAsync(Order order)
{
try
{
_repository.SaveAsync(order);
}
catch (DbException ex)
{
_logger.LogError(ex, "Save failed");
throw;
}
}
Find and fix the bug.
The call is missing its await, so the returned Task is discarded. An async method does not throw into the caller's frame — it captures the failure onto its Task and rethrows only when that task is awaited. Fix it by writing await _repository.SaveAsync(order);.
- ✗Calling an async method without
awaitand assuming failures still propagate - ✗Believing an
asyncmethod throws synchronously into the caller'stry/catch - ✗Treating the returned
Taskas optional and discarding it
- →What happens to an exception sitting on a
Taskthat nobody ever awaits? - →How would a compiler warning or an analyzer have caught this discarded task?
Solution
public async Task HandleAsync(Order order)
{
try
{
await _repository.SaveAsync(order); // ✅ the task is awaited — the failure is rethrown here
}
catch (DbException ex)
{
_logger.LogError(ex, "Save failed");
throw;
}
}
Why the original code stayed silent
An async method does not throw into its caller's frame. The compiler rewrites its body into a state machine: when SaveAsync hits the DbException, the machine catches it and stores it on the returned Task via SetException. The task moves to Faulted, and the exception simply sits there.
There is exactly one way to get it back out — await the task. await asks the task for its result, sees Faulted, and rethrows the stored failure (preserving the original stack trace via ExceptionDispatchInfo).
In the original code, _repository.SaveAsync(order); discards the returned Task. By the time the database rejects the write, execution has long left the try block, and the catch physically cannot intercept anything — nothing was thrown in that frame. The method returns a successfully completed task and the caller concludes the order was saved.
⚠️ Since .NET 4.5 a task whose exception nobody observes no longer crashes the process — the failure is lost quietly. That is precisely what makes a discarded Task more dangerous than a faulted one.
The compiler warns about this (CS4014), and analyzers such as CA2012 / VSTHRD110 flag discarded tasks as an error — do not suppress those rules.