What async/await Doesn't Tell You
Key takeaways
async/awaitalone doesn’t tell you when work starts or whether tasks overlap.- Receiving an error doesn’t necessarily mean other tasks have stopped.
- Structured concurrency ties child tasks to a scope responsible for their lifetimes.
- Cancellation can take time and doesn’t automatically undo changes.
Switch languages, spot async and await, and it’s tempting to assume you already know how the code behaves. That familiarity can hide downloads that outlive their screens or supposedly concurrent requests that run one after another. The keywords look reassuring; the execution rules deserve a closer look.
Two awaits, two different timelines
await marks a point where a function needs the result of an asynchronous operation. If that result isn’t ready, the function can suspend and resume later.
But when did the operation actually start?
In some designs, calling an asynchronous function starts executing it immediately. In others, the call creates an object representing work that still needs to be driven by a runtime or scheduler. Recognizing the syntax doesn’t tell you which model you’re reading.
Suppose two independent image downloads each take one second:
- Start both, then await their results in sequence: the total could be about one second.
- Start the first, await its result, then start the second: the total is about two seconds.
These are simplified timings. Real networks add delays, and downloads may compete for resources. The useful detail is still the same: when each operation starts determines whether their execution overlaps.
Counting await expressions won’t answer that question.
Overlapping work also doesn’t require multiple CPU cores. One task can make progress while another waits for a network response. Asynchronous execution alone tells you nothing about whether computations run in parallel.
An error can arrive while other work continues
Imagine a page fetching a profile and a notification list together. The profile succeeds; the notifications fail.
Several outcomes could make sense. The page might show the profile with an error in the notification panel. It might require both results and display a page-wide error. Or, if either request fails while the other is still running, it might request cancellation of the remaining work.
Those choices depend on the application and the mechanism used to group tasks. async/await doesn’t select a policy for you.
The distinction to watch is between error propagation and task cancellation. An error reaching the caller doesn’t establish that every related task has finished. Some work may still be running after the exception handler returns.
“There’s a catch block” is therefore an incomplete answer in a code review. You also need to know what happens to the other tasks.
Structured concurrency gives work an owner
Ordinary function calls have a readable shape: a function calls another function, waits for it to return, and continues.
Launching asynchronous work can make that relationship harder to follow. A function finishes building a screen, but a task it started keeps running. Who is responsible for that task now?
Structured concurrency ties child tasks to a scope that manages their lifetimes. Finishing the scope includes managing the termination of the tasks that belong to it.
For a search screen, that scope could own both the search-results request and the suggested-queries request. Closing the screen can then trigger their cleanup through a clearly defined owner.
That structure makes responsibility easier to trace. It doesn’t tell you every detail of failure handling.
You still need to check whether one child’s failure triggers cancellation of its siblings, how multiple errors reach the caller, and how cancellation is handled. Knowing which scope owns the work is the starting point for understanding those rules.
Cancellation has a delivery time—and cleanup costs
A Cancel button creates a fairly strong expectation. Underneath it, the code may only be sending a request.
With cooperative cancellation, a task has to observe that request and respond. It might check explicitly or encounter cancellation at a supported waiting point. A long computation that never checks can take time to react.
Timeouts need the same scrutiny. Stopping the wait for a result doesn’t necessarily stop the operation producing it. Check whether the timeout mechanism ends the wait, requests cancellation, or does both.
Even after a task begins stopping, cleanup remains. A download may need to close a file. Temporary data may need to be deleted. Completing those steps can take additional time.
Cancellation also doesn’t automatically reverse earlier effects. Bytes already written to a file remain unless code removes them or otherwise restores the previous state.
Follow cancellation through three stages: how the request reaches the task, where the task responds, and how the caller knows cleanup has finished. Each stage answers a different part of what “canceled” means.
When reading async code, trace when work starts, how failures travel, and where cleanup ends. A screen can say “Canceled” as soon as a request is sent; whether the work is actually finished is a separate fact.
Comments
Loading comments...