Typorithm · hard

4. Promises, cancellation and bounded concurrency

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

4. Promises, cancellation and bounded concurrency

0%

Concept and contract

Treat successful result types separately from timing, cancellation and failure.

Promise<T> describes a successful eventual value, not a guarantee of success or a cancellation protocol. Awaited can extract that value type, but rejected promises and request ordering remain runtime concerns. A precise return type does not prevent stale results from arriving after a newer request.

Cancellation is cooperative. AbortSignal communicates an abort request to APIs that honor it; it does not undo a completed write or automatically stop arbitrary JavaScript work. Check the contract of each underlying operation instead of treating abort as universal rollback.

Mechanism and worked example

The example uses Promise.allSettled for independent operations and narrows each settled result by status.

The example uses Promise.allSettled for independent operations and narrows each settled result by status. A fulfilled branch has value, while a rejected branch has reason. This lets a caller preserve partial success instead of losing all outcomes through the first rejection.

Promise.all is appropriate when the combined operation should fail if any member fails, but its rejection does not cancel the remaining promises. Avoid unbounded mapping over thousands of network tasks. A small worker pool or sequential batches can protect both your process and the provider.

Complete worked example

typescript

async function collect(tasks: readonly Promise<number>[]): Promise<number[]> {
  const results = await Promise.allSettled(tasks);
  return results.flatMap(result => result.status === "fulfilled" ? [result.value] : []);
}
Reasoning
The status discriminant determines whether a settled result contains a successful value.

Counterexamples and common mistakes

The rejected access reads value before narrowing the settled result.

The rejected access reads value before narrowing the settled result. A rejection does not have that property. Replacing the union with any would hide an outcome the application needs to handle, especially in partial-success imports or multi-source dashboards.

Do not interpret a timeout wrapper as proof the underlying operation stopped. It may only stop waiting. For idempotent reads, cancellation and stale-response guards can work together; writes need explicit retry and idempotency semantics.

Rejected counterexample: expected compiler diagnostic

typescript

function unsafe(result: PromiseSettledResult<number>): number {
  // @ts-expect-error Rejected outcomes do not have value.
  return result.value;
}
Reasoning
Reading value without checking status ignores the rejected alternative.

Independent practice

Implement settledCounts for a readonly collection of numeric promises.

Implement settledCounts for a readonly collection of numeric promises. Return fulfilled numbers and a failure count, preserving the fulfilled order. Begin with one success and one failure, then add empty-input behavior.

Test that a rejected promise does not erase the successful value. Optional extension: add a concurrency limit to a collection of task factories, not already-started promises. The basic task should remain a short exercise in tagged-outcome handling.

Step 1: Handle one fulfilled and one rejected result. Step 2: Collect values and failure count in input order.

Practice starter: solve before reading the solution

typescript

async function settledCounts(tasks: readonly Promise<number>[]): Promise<{values:number[];failures:number}> { throw new Error("TODO"); }
Reasoning
Keep fulfilled order and count failures without assuming the reason type.
  • Hint 1: Branch on settled status before reading value.
  • Hint 2: Already-started promises are not a concurrency-limited queue.

Solution, tests and trade-offs

The solution waits with allSettled and branches on status before reading value.

The solution waits with allSettled and branches on status before reading value. It does not assume a rejection reason is an Error. The returned counts summarize outcomes without logging arbitrary rejected values or claiming failed operations were canceled.

Run deterministic fulfilled and rejected promises. A compiler rejection protects access before narrowing. For a real network integration, add cancellation and resource-limit tests separately; this small aggregation function does not solve provider rate limiting by itself.

Practice solution and acceptance checks

typescript

async function settledCounts(tasks: readonly Promise<number>[]): Promise<{values:number[];failures:number}> {
  const values:number[]=[];let failures=0;
  for(const result of await Promise.allSettled(tasks)) { if(result.status === "fulfilled") values.push(result.value); else failures++; }
  return {values,failures};
}

// Acceptance checks
void (async()=>{const x=await settledCounts([Promise.resolve(0),Promise.reject("bad"),Promise.resolve(3)]);if(x.values.join(",")!=="0,3"||x.failures!==1||(await settledCounts([])).failures!==0)throw new Error("settled");})();
Reasoning
The outcome union supports partial success, but does not cancel or limit already-started work.

Interview reasoning and next step

In interviews, distinguish all from allSettled, promise rejection from cancellation, and stopping a wait from stopping work.

In interviews, distinguish all from allSettled, promise rejection from cancellation, and stopping a wait from stopping work. Explain how to bound concurrency before requests begin and why a type parameter cannot express all those runtime guarantees.

Next, use a state machine and request generation to prevent old results from overwriting current UI state. That problem remains even when every individual response has the correct static type.

Optional stretch — after the core task works: Add a bounded worker pool over task factories.

Treat successful result types separately from timing, cancellation and failure.