asynchronous programming
Task.Run
exception handling
C# programming
.NET

How to handle Task.Run Exception

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Exceptions thrown inside Task.Run are not lost, but they are also not raised immediately on the caller thread. They are captured by the returned Task and then rethrown when you await the task, access its result, or synchronously wait on it. That behavior is the key to handling Task.Run exceptions correctly.

The Normal Pattern: await Inside try and catch

The cleanest approach is to await the task inside a try and catch block:

csharp
1using System;
2using System.Threading.Tasks;
3
4public class Demo
5{
6    public static async Task Main()
7    {
8        try
9        {
10            await Task.Run(() =>
11            {
12                throw new InvalidOperationException("Boom");
13            });
14        }
15        catch (InvalidOperationException ex)
16        {
17            Console.WriteLine(ex.Message);
18        }
19    }
20}

The exception is thrown inside the background work, stored on the task, and then rethrown at the await. That means normal async exception handling works exactly where you await.

Why Ignoring the Returned Task Is Dangerous

If you call Task.Run(...) and never observe the returned task, you have created fire-and-forget work. At that point, exception handling becomes much harder because nothing is awaiting the failure path.

Bad pattern:

csharp
Task.Run(() => DoWork());  // exception may go unobserved

Better pattern:

csharp
Task task = Task.Run(() => DoWork());
await task;

If you truly need background work that outlives the current call, attach explicit logging or continuation behavior so failures are still observed.

Wait() and .Result Behave Differently

If you block synchronously with task.Wait() or task.Result, exceptions are wrapped in AggregateException:

csharp
1try
2{
3    Task.Run(() => throw new InvalidOperationException("Boom")).Wait();
4}
5catch (AggregateException ex)
6{
7    Console.WriteLine(ex.InnerException?.Message);
8}

That is one reason await is preferred. It unwraps the exception and produces clearer control flow.

Handle Multiple Tasks Carefully

When several tasks run in parallel, Task.WhenAll is usually the right coordination primitive:

csharp
1try
2{
3    await Task.WhenAll(
4        Task.Run(() => throw new InvalidOperationException("A")),
5        Task.Run(() => throw new ApplicationException("B"))
6    );
7}
8catch (Exception ex)
9{
10    Console.WriteLine(ex.Message);
11}

When awaited, Task.WhenAll throws if any child task fails. The aggregated task still contains the full set of failures in its Exception property if you need to inspect them all.

Use Task.Run for the Right Kind of Work

Task.Run is mainly for CPU-bound work that you want off the caller thread, especially in UI applications. It is not a universal fix for every async problem. If the underlying operation is already asynchronous, wrapping it in Task.Run often adds noise instead of value.

That matters for exception handling too. If you use Task.Run where ordinary async I/O would have been correct, you make the control flow harder to follow and create more places where task observation can be forgotten.

Fire-and-Forget Needs Explicit Error Handling

Sometimes fire-and-forget work is intentional. In that case, do not pretend the exception path does not exist. Catch and log inside the background delegate:

csharp
1Task.Run(() =>
2{
3    try
4    {
5        DoWork();
6    }
7    catch (Exception ex)
8    {
9        Console.WriteLine(ex);
10    }
11});

That is not better than awaiting. It is the fallback for cases where awaiting is not part of the design.

Common Pitfalls

  • Starting Task.Run and never observing the returned task.
  • Using .Wait() or .Result without accounting for AggregateException.
  • Expecting the exception to be thrown immediately at the call to Task.Run.
  • Wrapping naturally asynchronous I/O code in Task.Run unnecessarily.
  • Using fire-and-forget background work without explicit logging or recovery behavior.

Summary

  • Exceptions inside Task.Run are captured by the returned task.
  • The best handling pattern is await inside try and catch.
  • Synchronous waits wrap failures in AggregateException.
  • Unobserved fire-and-forget tasks make failures easy to miss.
  • Use Task.Run deliberately, mainly for CPU-bound work that should leave the caller thread free.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.