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.
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:
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:
the C# compiler generates state-machine code that:
- Gets the awaiter from the task-like object.
- Checks whether it is already complete.
- Registers the remainder of the async method through
OnCompletedorUnsafeOnCompleted. - Calls
GetResultwhen 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
awaitfor readability and correct exception flow. - Use
ContinueWithonly when you specifically need continuation chaining or scheduler control. - Avoid calling
OnCompleteddirectly 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
ContinueWithas equivalent toawaitleads to subtle bugs around scheduler choice and exception propagation. - Calling
OnCompleteddirectly withoutGetResulthides task failures because completion and successful completion are not the same thing. - Forgetting that
ContinueWithreceives the completed task encourages unsafe access toResultbefore 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
ContinueWithare explicit continuations attached to aTask. - '
OnCompletedis a lower-level awaiter hook used by the async state machine behindawait.' - '
awaitis usually preferable because it handles flow, exceptions, and readability better.' - Reach for direct
OnCompletedusage only in advanced async infrastructure code.

