Custom Task-like Types (Extending async in CSharp)
🎯 Why Would You Want a Custom Awaitable?
Before diving into the code, let's answer the why. Why build your own awaitable
type instead of using the tried-and-true Task
?
Use Case 🔍 | Scenario 💡 |
---|---|
Performance | You want to avoid heap allocation (common with Task ) |
Control | You want full control over the scheduling, timing, or thread context |
Domain-specific design | Build something tailored to game loops, coroutines, or embedded systems |
Interoperability | Integrate with async APIs from other platforms (Unity, embedded C++, etc.) |
🧠 Understanding the Await Pattern in C#
For a type to be await
-able, it must conform to the “awaitable pattern”, which means:
- It must have a method called
GetAwaiter()
. - The object returned from
GetAwaiter()
must:- Implement
INotifyCompletion
orICriticalNotifyCompletion
- Have:
IsCompleted
property (bool)GetResult()
methodOnCompleted(Action)
method
- Implement
✅ This means you don’t need to derive from
Task
orTask<T>
— just follow the pattern!
🧪 Let's Build One: MyAwaitable
We'll now build a minimal custom awaitable type.
1️⃣ Create the Awaitable Wrapper
2️⃣ Create the Awaiter
3️⃣ Use it with await
🔍 Behavior Breakdown
Method Purpose
- IsCompleted Returns true if the result is already available (synchronous path)
- OnCompleted(Action) Called if IsCompleted == false, used to register continuation
- GetResult() Called after resumption, returns the actual result (if any)
🛠 Advanced: ValueTask and Performance
Ever heard of ValueTask<T>? It's a real-world example of a task-like type. The idea is simple:
If you already have the result, don’t allocate a Task
Save GC pressure for high-throughput code (like I/O systems or networking)
You can implement a custom struct-based awaitable for zero allocations by using ValueTask, or even your own:
And your MyFastAwaiter could avoid allocating closures or heap objects entirely.
🎮 Real-World Use Cases
Domain Usage
Game Engines Unity uses custom awaitables for coroutines and frame-bound execution
High-performance servers Use ValueTask or custom types to reduce memory pressure
Embedded/IoT Implement fake async behavior in non-threaded environments
Testing Simulate async behavior without using threads
⚠️ Limitations and Warnings
Creating custom awaitables is powerful but also has caveats:
⚠️ Debugging becomes harder – Stack traces and async flow tracking may be harder to follow
🔍 You’re on your own – Compiler won’t help you much if you mess up GetResult() or scheduling
🧪 Complexity rises fast – If you're not gaining performance or clarity, just stick to Task
✅ Summary
Feature Supported
Works with async/await ✅
Requires inheritance? ❌ (just follow the pattern)
Can return values ✅ (GetResult() can return any type)
GC friendly ✅ if using structs or pooled implementations
🧠 Conclusion
Custom Task-like types are one of the most underrated features of C#. They allow you to fine-tune your async experience, improve performance, and build domain-specific abstractions without being tied to Task.
Whether you’re optimizing game loops, building a reactive UI layer, or running highly performant servers, knowing how to build your own awaitables can take your C# skills to the next level. 🚀