Async Programming
.NET
Task Callbacks
OnCompleted
Software Development

Difference between Task Callbacks and OnCompleted

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In .NET async code, several APIs let you run code after a task finishes, but they do not all exist at the same abstraction level. A task continuation such as ContinueWith is an application-facing callback mechanism, while OnCompleted belongs to the lower-level awaiter pattern that powers await under the hood.

Task Callbacks With ContinueWith

When developers say "task callback," they usually mean attaching a continuation to a Task. The continuation runs after the original task completes.

csharp
1using System;
2using System.Threading.Tasks;
3
4public static class ContinueWithDemo
5{
6    public static async Task Main()
7    {
8        Task<int> work = Task.Run(async () =>
9        {
10            await Task.Delay(100);
11            return 42;
12        });
13
14        Task continuation = work.ContinueWith(t =>
15        {
16            if (t.IsFaulted)
17            {
18                Console.WriteLine(t.Exception);
19                return;
20            }
21
22            Console.WriteLine($"Result: {t.Result}");
23        });
24
25        await continuation;
26    }
27}

This style gives you direct access to the completed task, including Result, Exception, and status flags. It is explicit, but it also places more responsibility on you to reason about schedulers, error handling, and nested tasks.

What OnCompleted Actually Is

OnCompleted is not a higher-level alternative to ContinueWith. It is a method on an awaiter that lets the awaiter schedule the continuation used by the async state machine.

You can call it directly, but that is rarely how application code should be written:

csharp
1using System;
2using System.Threading.Tasks;
3
4public static class AwaiterDemo
5{
6    public static Task Main()
7    {
8        var task = Task.Delay(100);
9        var awaiter = task.GetAwaiter();
10        var completion = new TaskCompletionSource<bool>();
11
12        awaiter.OnCompleted(() =>
13        {
14            try
15            {
16                awaiter.GetResult();
17                Console.WriteLine("Task finished");
18                completion.SetResult(true);
19            }
20            catch (Exception ex)
21            {
22                completion.SetException(ex);
23            }
24        });
25
26        return completion.Task;
27    }
28}

That callback does not receive the original Task as an argument. Instead, it relies on the awaiter and must call GetResult to observe completion or propagate exceptions.

How await Uses OnCompleted

When you write:

csharp
await SomeOperationAsync();

the C# compiler generates state-machine code that:

  1. Gets the awaiter from the task-like object.
  2. Checks whether it is already complete.
  3. Registers the remainder of the async method through OnCompleted or UnsafeOnCompleted.
  4. Calls GetResult when execution resumes.

That means OnCompleted is mostly infrastructure. It exists so the language can suspend and resume async methods in a standard way.

When to Use Which

In normal application code:

  • Prefer await for readability and correct exception flow.
  • Use ContinueWith only when you specifically need continuation chaining or scheduler control.
  • Avoid calling OnCompleted directly unless you are implementing low-level async infrastructure, custom awaitables, or diagnostic experiments.

await is usually the right answer because it is easier to compose and easier to read. ContinueWith is more manual. OnCompleted is more primitive still.

One useful mental model is that ContinueWith is a library API you call yourself, while OnCompleted is usually a compiler-facing hook that async infrastructure exposes.

Common Pitfalls

  • Treating ContinueWith as equivalent to await leads to subtle bugs around scheduler choice and exception propagation.
  • Calling OnCompleted directly without GetResult hides task failures because completion and successful completion are not the same thing.
  • Forgetting that ContinueWith receives the completed task encourages unsafe access to Result before checking for faults.
  • Using low-level awaiter APIs in ordinary business logic makes async code harder to maintain than it needs to be.

Summary

  • Task callbacks such as ContinueWith are explicit continuations attached to a Task.
  • 'OnCompleted is a lower-level awaiter hook used by the async state machine behind await.'
  • 'await is usually preferable because it handles flow, exceptions, and readability better.'
  • Reach for direct OnCompleted usage only in advanced async infrastructure code.

Course illustration
Course illustration

All Rights Reserved.