Async .NET · Lesson 20

Timeouts and caller cancellation are different outcomes

Use a timeout for the wait without pretending the underlying operation stopped.

How it works

A timeout describes how long a caller will wait for completion. It is a different outcome from an explicit caller cancellation request and from an operation fault. WaitAsync with a timeout creates a wait that can fail with TimeoutException even though the original task has not finished.

TimeSpan.Zero makes the teaching example deterministic: the incomplete operation cannot satisfy an immediate wait, so the timeout path runs. The original task still reports incomplete. It can later receive forty-two and complete successfully, demonstrating that timing out the wrapper did not stop or fault the operation itself.

A real timeout value should come from a documented policy rather than a convenient number copied from an example. Network latency, user expectations, retries, and total workflow budgets are different considerations. This local demonstration intentionally avoids pretending that a tiny timer duration is a reliable measurement of performance.

If the operation supports cancellation, a workflow may request its cancellation after a timeout and then wait for cooperative cleanup. That is an additional action with its own lifetime implications. A timeout cannot forcibly guarantee that every external effect has ceased, especially if the operation does not observe the supplied token.

Practice by completing operation before calling WaitAsync with the zero timeout. Predict why the already-available result can be returned. Then add an explicit caller token and handle OperationCanceledException separately from TimeoutException, choosing messages that accurately describe which wait policy ended the caller's attempt.

Finally, keep a reference to the original task after a timed-out wait and observe its later success or failure. Decide which component owns that observation if the original caller has already returned. This accounting prevents an apparent timeout recovery from quietly creating detached work whose eventual exceptions and resource use nobody tracks.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

var operation = new TaskCompletionSource<int>();
try { await operation.Task.WaitAsync(TimeSpan.Zero); }
catch (TimeoutException) { Console.WriteLine("timed out"); }
Console.WriteLine(operation.Task.IsCompleted);
operation.SetResult(42);
Console.WriteLine(await operation.Task);

Expected output

timed out
False
42
Try this
A timeout ends this wait; retain ownership of the unfinished operation.