Lessons in this course (9)

4. Exceptions in async, parallel and background code

Intermediate★ Must knowLesson 4 of 9
csharp
// await rethrows the first inner exception directly
try { await http.GetStringAsync(url); }
catch (HttpRequestException ex) { /* handled */ }

// Task.WhenAll: await throws the FIRST exception; all are in the task's AggregateException
var all = Task.WhenAll(tasks);
try { await all; }
catch { foreach (var ex in all.Exception!.InnerExceptions) logger.LogError(ex, "Task failed"); }

// .Wait() / .Result wrap in AggregateException (and can deadlock) — avoid
// Parallel.ForEach collects exceptions into AggregateException
try { Parallel.ForEach(items, Process); }
catch (AggregateException ae) { ae.Handle(ex => ex is InvalidDataException); }   // handle some, rethrow the rest

// async void: exceptions crash the process (can't be awaited/caught) — only for event handlers

Background services — an unhandled exception in ExecuteAsync stops the host by default (.NET 6+, BackgroundServiceExceptionBehavior.StopHost). Wrap each iteration:

csharp
protected override async Task ExecuteAsync(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        try { await ProcessBatchAsync(ct); }
        catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; }
        catch (Exception ex) { logger.LogError(ex, "Batch failed; retrying in 30 s"); }
        await Task.Delay(TimeSpan.FromSeconds(30), ct);
    }
}

Last-chance handlers (log, then let the process die):

csharp
AppDomain.CurrentDomain.UnhandledException += (s, e) => Log.Fatal((Exception)e.ExceptionObject, "Unhandled exception");
TaskScheduler.UnobservedTaskException += (s, e) => { Log.Error(e.Exception, "Unobserved task exception"); e.SetObserved(); };