Are nested completion blocks a sign of bad design?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Nested completion blocks are a common symptom of asynchronous code that grew organically. They are not automatically wrong, but deep nesting usually signals that orchestration, transformation, and error policy are mixed in one place. The better question is not whether nesting exists, but whether the code clearly communicates control flow and failure behavior.
Why Nesting Appears In Real Projects
Most teams start with a single asynchronous call, then add one more step, then another. At first, callbacks feel direct because each operation is close to the next one. Over time, three issues appear.
First, error handling becomes fragmented. One callback logs and continues, another callback returns, and a third callback retries. A reader cannot tell the true failure policy without scanning every branch.
Second, cancellation and timeout logic gets bolted on late. If each callback can schedule new work, it becomes easy to continue doing expensive operations after the caller already gave up.
Third, test setup becomes heavy. To test one branch, you must build many fake responses and timing conditions. That usually means fewer tests, then higher regression risk.
Deep nesting therefore acts as a design signal. It tells you orchestration is now complex enough to deserve named steps.
When Nested Completion Blocks Are Fine
Small, local nesting can still be readable. For example, a UI action might call one network function, then update local state. If there are only one or two levels and one clear error path, this is often acceptable.
The practical rule is to track cognitive load, not count indentation levels. If a reviewer can answer these questions quickly, the design is likely still healthy.
- Which operation starts first.
- Which operation depends on previous output.
- What happens on failure.
- What stops pending work.
If those answers are not obvious, nesting is no longer just style. It is a maintainability issue.
Refactor To Named Steps And A Single Error Policy
A good refactor keeps behavior the same first, then improves structure. Extract each asynchronous responsibility into a function with a narrow contract. After that, choose one orchestration style per boundary.
Example with completion handlers in Swift:
This still has nesting, but each function has one job and one Result contract. That already makes debugging easier.
If your platform supports structured concurrency, move orchestration to async functions.
The async version removes callback pyramid shape and centralizes error propagation with throw.
Guardrails For Production Code
After refactoring, add constraints that protect behavior under load.
- Add timeouts at I O boundaries.
- Log a request identifier through all async steps.
- Guarantee completion is called once in callback APIs.
- Define cancellation behavior and test it.
These guardrails matter more than stylistic purity. Most incidents come from inconsistent failure handling, not from indentation depth.
Common Pitfalls
- Treating all nesting as bad design, then introducing over engineered abstractions.
- Mixing callback style and
asyncstyle in one function without a clear boundary. - Calling completion more than once on race conditions.
- Ignoring cancellation propagation when the caller no longer needs results.
- Refactoring structure without adding tests for timeout and failure paths.
Summary
- Nested completion blocks are a signal, not an automatic defect.
- Short local nesting can be clear when dependencies and errors are obvious.
- Complexity should be handled by named async steps with a single contract.
- Structured concurrency improves readability and testability for orchestration.
- Reliability depends on timeout, cancellation, and once only completion guarantees.

