Non-Generic TaskCompletionSource or alternative
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
In modern .NET, TaskCompletionSource<T> is generic, and there is no separate non-generic TaskCompletionSource type in standard APIs. When you need a completion signal without a payload, there are clear alternatives that keep intent explicit. The best choice depends on whether you need result data, cancellation, or simple event-like completion.
Core Sections
Why there is no non-generic built-in type
The runtime API is centered on generic TaskCompletionSource<T>. If you only need completion, use a placeholder type for T such as bool, object?, or a custom marker.
bool is often easiest for signal-only completion.
Signal-only alternatives
Common signal-only patterns include:
TaskCompletionSource<bool>TaskCompletionSource<object?>withnullresultSemaphoreSlimfor count-based signalingChannelfor streaming or multiple notifications
Use primitive synchronization only when task semantics are insufficient.
Using TaskCompletionSource<object?>
This can be convenient when you want semantic “no payload” completion.
This reads naturally and avoids artificial boolean values.
Cancellation and error propagation
Remember that TaskCompletionSource models all completion states.
Use TrySet* methods to avoid exceptions from double completion races.
Validation and production readiness
Double completion is a common bug in event-driven systems. Guard handlers so completion happens exactly once, then add tests that simulate duplicate callbacks and out-of-order events. Include timeout-based tests to confirm tasks do not hang forever under missed signals.
Use RunContinuationsAsynchronously in most service code to avoid inline continuation surprises and lock inversion bugs. This option keeps callback chains more predictable under load.
For observability, log lifecycle transitions at key boundaries: pending, completed, canceled, and faulted. This is useful when diagnosing sporadic deadlocks or timing races in integration environments.
Validation and production readiness
A practical solution should be verified under realistic conditions, not just a single local run. Build a compact test matrix with expected inputs, boundary values, malformed cases, and one representative high-volume scenario. This catches many defects early, including hidden assumptions about ordering, type conversion, timing, and error propagation. If the implementation interacts with external systems, include at least one test where a dependency is unavailable and confirm the failure mode is explicit and observable.
Use deterministic checks wherever possible. For data processing flows, assert row counts, key uniqueness, and output schema. For asynchronous flows, assert completion timing boundaries and cancellation behavior. For security-sensitive operations, assert deny-by-default behavior when configuration is missing or invalid. Do not rely on visual inspection alone. Codified assertions are faster to run and easier to maintain.
Observability is part of correctness. Emit structured logs around key decision points so failures can be diagnosed without reproducing the entire scenario manually. Include identifiers, operation outcome, and duration in a consistent format. Avoid sensitive payloads in logs. For long-running jobs, add periodic progress events and final summary counters so stalled states can be detected quickly.
Configuration should be explicit and versioned. Keep environment-dependent values external and validate them at startup. If a required variable is absent, fail fast with clear messaging instead of silently applying weak defaults. Document compatible runtime versions and dependency constraints near the code to reduce environment drift between local machines and CI runners.
Before release, apply a lightweight operational checklist. Confirm rollback steps, monitor thresholds, and idempotency expectations. If a task can run more than once, ensure repeated execution does not corrupt state or duplicate side effects. Teams that standardize this discipline usually reduce incident frequency and spend less time on reactive debugging.
Common Pitfalls
- Searching for a non-generic
TaskCompletionSourcetype that does not exist. - Using
SetResultand crashing on duplicate completion races. - Forgetting cancellation and exception paths, causing hung awaits.
- Omitting
RunContinuationsAsynchronouslyin callback-heavy flows. - Modeling stream-like events with a single completion primitive.
Summary
- .NET provides
TaskCompletionSource<T>, not a non-generic variant. - For signal-only completion, use
TaskCompletionSource<bool>orTaskCompletionSource<object?>. - Always handle success, cancel, and error completion states.
- Prefer
TrySet*and asynchronous continuations for robustness. - Add race and timeout tests to validate real-world behavior.

