Define an abstract async method even if not all overrides are async?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
In C#, the right abstraction is usually to define an abstract method that returns Task or Task<T>, not an abstract method marked async. The async keyword is part of the implementation body, while the asynchronous contract is expressed by the return type, which means some overrides can use await and others can return a completed task directly.
async Is Not the Contract
This is the key idea: async is a compiler feature for a method body. It is not part of the polymorphic contract in the way the return type is.
That means an abstract declaration should usually look like this:
Not like this:
- “abstract async method”
An abstract method has no body, so there is nowhere for await to live. The asynchronous shape of the API is already captured by Task.
Overrides Can Be Truly Async or Synchronously Completed
Some derived classes may need real asynchronous work:
Other derived classes may complete immediately and not need async at all:
Both are valid overrides of the same abstract contract.
Why This Design Is Useful
Returning Task from the base contract gives callers one uniform usage pattern:
The caller does not need to know whether the derived implementation:
- actually awaited I/O
- returned
Task.CompletedTask - used
Task.FromResult
That is exactly what a good abstraction should provide.
Use Task<T> for Asynchronous Results
If the abstract method produces a value, use Task<T>:
Async override:
Immediate override:
Again, the return type defines the async-friendly contract. The async keyword only appears when an implementation needs it.
Do Not Add async Without await
If an override does not need to await anything, do not mark it async just for consistency. That adds an unnecessary state machine and can make the code noisier.
Bad habit:
Better:
This keeps the implementation honest about what it is actually doing.
Interfaces Follow the Same Rule
The same design principle applies to interfaces:
Implementations can choose whether they are internally asynchronous or immediately completed. The interface contract remains clean and consistent either way.
A Good Rule of Thumb
Use async-shaped contracts when callers should be able to await the operation, regardless of whether every implementation performs asynchronous work internally.
That is common when:
- some implementations use I/O
- some implementations use cached or in-memory results
- you want one API shape for all derived types
This is one of the places where Task is valuable even when a specific implementation finishes synchronously.
Common Pitfalls
- Thinking the abstract declaration itself should be marked
asyncrather than just returningTask. - Marking overrides
asynceven when they do not await anything. - Returning
voidfrom an abstract asynchronous contract instead ofTask, except for event handlers. - Exposing both sync and async polymorphic methods without a clear need and making the API harder to use.
- Forgetting that
Task.CompletedTaskandTask.FromResultare the normal tools for synchronously completed overrides.
Summary
- Abstract async-style methods in C# should normally return
TaskorTask<T>. - The
asynckeyword belongs in implementations that actually useawait. - Some overrides can be truly asynchronous, while others can return completed tasks immediately.
- This keeps the caller-facing contract uniform without forcing every implementation to be asynchronous internally.
- Treat
asyncas an implementation detail andTaskas the API contract.

