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 💡
PerformanceYou want to avoid heap allocation (common with Task)
ControlYou want full control over the scheduling, timing, or thread context
Domain-specific designBuild something tailored to game loops, coroutines, or embedded systems
InteroperabilityIntegrate 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:

  1. It must have a method called GetAwaiter().
  2. The object returned from GetAwaiter() must:
    • Implement INotifyCompletion or ICriticalNotifyCompletion
    • Have:
      • IsCompleted property (bool)
      • GetResult() method
      • OnCompleted(Action) method

✅ This means you don’t need to derive from Task or Task<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. 🚀


An unhandled error has occurred. Reload 🗙