وب مدرن، رابط کاربری و تجربه‌ی وب · فصل ۳ — Type System، Runtime، Async و Build · 43

Async JavaScript, Timeout, and Concurrency Limits

Choose sequential versus concurrent execution deliberately, enforce an observable concurrency budget, model timeout/cancellation separately, and prove in-flight count plus failure behavior with a deterministic local fixture. می‌تواند «Async JavaScript, Timeout و Concurrency Limit» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

Async JavaScript, Timeout و Concurrency Limit — fa-IR

Lesson summary

Choose sequential versus concurrent execution deliberately, enforce an observable concurrency budget, model timeout/cancellation separately, and prove in-flight count plus failure behavior with a deterministic local fixture.

می‌تواند «Async JavaScript, Timeout و Concurrency Limit» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

This lesson produces

می‌تواند «Async JavaScript, Timeout و Concurrency Limit» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

Who it is for

Foundation

Worked baseline, explicit evidence table, safe local fixture and one guided failure probe.

Repair

Reduce to one failing contract, observe evidence, repair, then restore full case.

Experienced

Skip syntax walkthrough; require independent implementation, mutation evidence and residual-risk statement.

Prerequisites

Fast is not the same as controlled

A page needs to process a batch of local work items. Each item is independent, each has a deadline, and the downstream resource can safely handle only a small number of active operations at once. Two implementations can both “use async/await” and still behave very differently: one can serialize everything accidentally; another can start everything immediately and exceed the resource budget. A third can report “timeout” while the original operation keeps running in the background.

This lesson owns that boundary. You will build an Async Failure & Concurrency Budget Record v1 that makes four decisions inspectable: what is sequential and what may overlap, how many operations may be in flight, what a timeout result means, and whether cancellation actually reaches the underlying task. Completion requires measured evidence, a deliberate failure mutation, a repair, and an independent changed-context transfer.

The target is not “make promises finish.” The target is a scheduling contract you can defend. async and await are Promise-based control-flow tools; they do not declare that work is parallel. Promise.all() aggregates already-started or already-created work; it is not a resource governor. A timeout race changes what the caller observes, but it does not stop work unless the operation cooperates with a cancellation signal. [S-MDN-ASYNC] [S-MDN-PROMISE] [S-MDN-ABORT]

Scope, prerequisites, and completion evidence

WEB-001 is a hard conceptual prerequisite and WEB-002 is a soft prerequisite. Reuse their bounded evidence when you need language/runtime context; do not re-teach TypeScript or module resolution here. WEB-014 owns build-time code splitting. This lesson stays on runtime task scheduling, concurrency budgeting, timeout observation, cooperative cancellation, and failure evidence.

Your completion artifact is Async Failure & Concurrency Budget Record v1. It must record the scheduling strategy, configured concurrency limit, observed peak in-flight count, settlement behavior, timeout policy, whether the underlying operation observed a cancellation signal, at least one mutation prediction/observation/diagnosis/repair cycle, changed-context transfer, and one residual limitation.

Foundation route: keep the contract checklist visible and use the guided fixture. Repair route: reduce the problem to one failing dimension, rerun the smallest probe, then restore the full case. Experienced route: compress syntax review, but do not skip measured peak concurrency, timeout-versus-cancellation evidence, failure mutation, or transfer.

Mental model: queue → budget → active work → deadline/cancel signal → settlement → evidence

Think of the feature as a flow with explicit boundaries:

Queued work → scheduler/budget → active promises → timeout/cancel signal → settlement → measured in-flight/failure evidence.

The queue is work that has not started. The scheduler decides when an item may cross into the active set. The concurrency budget is an upper bound on that active set, not a suggestion. A deadline is a policy about how long the caller is willing to wait. Cancellation is a separate signal that the underlying task may or may not support. Settlement tells you whether the caller observed fulfillment or rejection; it does not by itself prove that background work stopped.

Use one counter for inFlight and one for peakInFlight. Increment immediately before starting the simulated resource work and decrement in a finally path. That measurement is more useful than visually watching log lines. If the contract says limit = 3, a baseline run with peakInFlight = 7 is a contract failure even if every item eventually succeeds.

For deadlines, ask two questions separately: what did the caller observe? and what happened to the operation? A Promise race can make the caller observe a timeout first. If the original work has no abort-aware path, it can continue after the race has settled. AbortSignal provides a way to communicate cancellation; an operation must actually listen to or otherwise honor that signal for cancellation to affect the work. [S-MDN-ABORT]

Foundation: four contracts to keep separate

1. Async control-flow contract

Calling an async function returns a Promise. await pauses that async function's continuation until the awaited value settles; it does not freeze the JavaScript environment and it does not promise parallel execution. If you place await inside a loop, you can intentionally or accidentally make independent operations sequential. If you create many operations before awaiting them, they may overlap. The decision belongs to your dependency and failure semantics, not to the keyword. [S-MDN-ASYNC]

2. Concurrency-choice contract

Sequential execution is appropriate when item B depends on item A, ordering is part of the requirement, or a resource permits only one active operation. Concurrent aggregation is appropriate when operations are independent and the resource can accept overlap. Promise.all() and related static methods describe how a group of supplied promises settles; they do not meter how many operations you start. [S-MDN-PROMISE]

3. Concurrency-budget contract

When a service, browser resource, device, or local simulator has a safe budget, the scheduler must decide when work starts. A common bounded pattern is a small worker pool: create limit worker loops, have each worker pull the next queued item, and await each assigned task before pulling another. The important property is observable: peakInFlight <= limit for the measured operation boundary.

The number itself is a product/resource decision, not a universal JavaScript constant. A limit of 3 in the lab is evidence for this fixture only. Do not turn it into “three is the best concurrency.”

4. Timeout/cancellation contract

A timeout can mean “the caller stops waiting after N milliseconds.” Cancellation means “the operation receives a signal to stop and cooperates.” These are compatible but not identical. A correct record therefore names both the deadline outcome and whether the task observed a signal. If the work is not abort-aware, report the timeout without claiming the work was cancelled.

Deep explanation: where accidental serialization and fan-out come from

Consider six independent items. This loop is easy to read:

for (const item of items) {
  await doWork(item);
}

It has a strong property: at most one doWork call is active. It also has a cost: independent work cannot overlap. If every item takes roughly 100 ms, six items require roughly six task durations rather than approximately two waves under a limit of three. That may be exactly right when ordering is required, but it is accidental serialization when no dependency exists.

Now consider:

await Promise.all(items.map(doWork));

This usually starts one operation for every item immediately because map invokes doWork for every element before Promise.all waits for the resulting promises. For six items, the measured peak can be six. Promise.all then fulfills when all fulfill or rejects when one input promise rejects. That settlement rule is useful, but it is not a concurrency limit. [S-MDN-PROMISE]

A bounded scheduler separates creation of work from admission to the active set. One worker completes an item before pulling another, while multiple workers may run at once. With three workers and six items, the peak should stay at or below three. This gives you a contract you can measure and mutate.

Timeout code has a similar hidden boundary. A common wrapper races doWork() against a timer. If the timer rejects first, the wrapper settles with a timeout. But doWork() still owns whatever timer, fetch, stream, or resource it started. To claim cancellation, pass an abort signal into an operation that supports it and verify that the operation observes the signal. AbortSignal.timeout() can create a time-based signal in supporting environments, but the same principle holds: signalling is only useful when the underlying operation responds. [S-MDN-ABORT]

Error handling also belongs to the chosen contract. Promise.all is fail-fast from the aggregate caller's perspective, but already-started sibling operations are not magically rolled back. A worker pool can choose to stop admitting new work after a failure, continue collecting results, or abort active work if the operations support it. State that policy instead of allowing it to be an accident of implementation.

Worked example: six jobs, limit three, one deadline

The lab fixture creates deterministic local jobs with counters and short delays. Start by measuring three strategies against the same work list.

Sequential baseline. Predict peakInFlight = 1. Run the strategy and record elapsed time, completion order, and settlements. This is your reference for intentional serialization.

Immediate aggregation. Replace the scheduler with immediate Promise.all fan-out. Before running, predict that the peak will rise to the number of items started together. Observe the counter. If the limit is 3 and the peak is 6, the failure is not “Promise.all is bad”; it is “this admission strategy violates this fixture's concurrency budget.”

Bounded worker pool. Run the same items through a scheduler configured with limit = 3. Record peakInFlight. A successful run must keep the measured peak at or below three while still allowing independent items to overlap.

Then add a deadline case. Use one abort-aware simulated task. Create a timeout signal or controller/timer, pass its signal into the task, and record two facts: the caller observed a timeout/abort outcome, and the task recorded that it received the signal before normal completion. Run a contrasting uncooperative task that ignores the signal. The caller may still observe a timeout, but your evidence should show that the underlying work continued. That difference is the point of the exercise.

Fill WEB-003-TAB1 row by row. A strong row includes contract, implementation mechanism, prediction, observed evidence, diagnosis, repair, post-repair evidence, and residual limitation. Avoid replacing observation with adjectives such as “fast,” “safe,” or “cancelled.”

Failure modes: three mutations that should change your conclusion

Mutation A — unbounded fan-out

From a working bounded scheduler, bypass it and start every independent job immediately with Promise.all. Predict the peak before running. If the fixture has eight items and budget three, peakInFlight should exceed three. Repair by restoring admission control, not by hiding the counter or increasing the declared budget after the fact.

Mutation B — timeout without cooperation

Keep the timeout wrapper but remove signal handling from the underlying task. Predict two observations: the caller still reports a deadline outcome, and the task continues long enough to record normal completion. Diagnose this as timeout without cancellation. Repair by wiring a signal into an operation that supports cooperative abort, then observe the task's abort path.

Mutation C — accidental serialization

Move an await into a loop over otherwise independent work. Predict peakInFlight = 1 and longer total elapsed time. This is not automatically incorrect; it is incorrect only if the owned requirement permits overlap and expects the bounded budget to be used. Repair by restoring bounded overlap while preserving the declared limit.

These probes protect against four plausible shortcuts: “async means parallel,” “Promise.all limits concurrency,” “timeout means cancelled,” and “more concurrency is always better.” Your final record should contain enough evidence that each shortcut can be rejected without appealing to style preference.

Experienced bridge: make scheduling policy explicit

If you already use promises daily, focus on policy rather than syntax. Write down these decisions before implementation:

  1. Which operations are independent enough to overlap?
  2. What resource defines the concurrency budget?
  3. Is the budget an upper bound, a target, or both? This lesson requires an upper bound.
  4. What happens after one task fails: stop admission, collect all outcomes, or abort active work?
  5. What does “timeout” mean to the caller?
  6. Which underlying operations actually support cancellation, and what evidence proves that they observed it?
  7. Which metrics are observable without inventing browser/network evidence?

Then instrument the boundary. Count active resource operations, not queued promises. Record settlement and abort events with item IDs. Keep timing comparisons qualitative unless your fixture controls the delays; general performance conclusions need broader evidence than a small local simulation.

Experienced code often hides scheduling inside helpers, retry libraries, SDKs, or request clients. That does not remove the contract. It moves where you must inspect it. If an SDK already implements a pool or queue, verify its documented/observed behavior instead of wrapping it with a second limiter by reflex.

Alternatives and trade-offs

Sequential loop

Strengths: simplest ordering model, peak concurrency of one, straightforward failure location. Costs: no overlap for independent work. Choose it when dependency/order/resource constraints justify it, not because it is the easiest await placement.

Immediate Promise.all

Strengths: concise aggregation and clear all-or-reject settlement for already-created work. Costs: it does not constrain how many operations you start, and one rejection does not cancel siblings automatically. Choose it only when unbounded-to-batch-size fan-out is acceptable or when another layer already controls admission.

Bounded worker pool

Strengths: explicit admission control and measurable peak. Costs: more scheduling code and a failure policy you must define. It is appropriate when the resource has a budget. A semaphore/limiter library can provide the same concept; the lab uses a small local implementation so the evidence boundary stays visible.

Timer race versus abort-aware deadline

A timer race is enough when the product contract is strictly “stop waiting and ignore any late result,” and the underlying work is harmless if it continues. An abort-aware deadline is needed when the product contract says the work itself should stop and the operation supports cancellation. Do not claim stronger semantics than the mechanism provides.

Fail-fast versus collect-all

Promise.all rejects on the first rejection observed by the aggregate. Promise.allSettled waits for every supplied promise and reports each outcome. Neither is a concurrency budget. Pick settlement semantics and admission control as two separate decisions.

Guided practice, then changed-context transfer

Open lab://WEB-003/WEB003-LAB1. The learner starter intentionally contains two weak mechanisms: an all-at-once scheduler and a timeout race that does not carry cancellation into the task. Before editing, run the public structural checks, inspect the simulator, and write predictions for peak in-flight count and deadline behavior.

Guided repair: implement a scheduler whose active count never exceeds the configured limit. Keep the public function interface stable. Then implement the deadline path so an abort-aware task receives a signal. Run the fixture and fill the baseline evidence rows. Use the mutation probe and record at least one full prediction → observation → diagnosis → repair → post-repair sequence.

Independent transfer: the transfer fixture changes item count, delays, failure placement, and the allowed concurrency budget. Do not copy the literal measurements from the guided case. Re-derive the expected peak, choose the failure policy, implement the same contract, and complete a separate transfer block in the evidence record without a decisive hint.

If your environment cannot provide a real browser observation, do not invent it. The local Node-compatible checks can prove the scheduling core and counters, while browser/DOM claims remain explicitly unclaimed until observed in a browser.

Verification: what counts as evidence

Your record must separate observation from inference. Example: “counter reached 3 and never exceeded 3 during this deterministic run” is observation. “the scheduler enforced the configured upper bound for this fixture” is a bounded inference supported by that observation. “this will never overload any production API” is unsupported.

Minimum evidence rows:

  • sequential strategy: observed peak and settlement result;
  • unbounded aggregation mutation: predicted and observed peak;
  • bounded scheduler: configured limit and observed peak;
  • timeout with cooperative signal: caller outcome and underlying abort observation;
  • timeout without cooperation: caller timeout plus late underlying completion;
  • one injected failure: chosen failure policy and observed sibling behavior;
  • repaired baseline: post-repair evidence;
  • changed-context transfer: independent measurements;
  • residual limitation: one condition the fixture does not prove.

The lab is deterministic enough to test the contract, not to benchmark JavaScript performance. Timer scheduling, browser load, real network behavior, server rate limits, and third-party cancellation semantics can differ. State that boundary.

Assessment and transfer

S1 — retrieval. State the core scheduling contract and one non-goal. A strong answer distinguishes async control flow from concurrency and does not require mutation evidence for this retrieval-only statement.

S2 — application. Implement the bounded feature. Submit measured peak in-flight evidence, settlement behavior, and deadline/cancellation evidence in the artifact.

S3 — diagnosis. Run one deliberate mutation. Predict the failure first, observe it, name the attacked dimension, repair it, and show post-repair evidence.

S4 — decision. Defend your mechanism against a plausible alternative. The answer must connect the choice to dependency, resource budget, failure semantics, and cancellation support rather than declaring one JavaScript pattern universally best.

S5 — transfer. Solve the changed fixture independently. Recalculate the expected bound and deadline behavior from the changed requirements. Transfer is blocking: copying the guided numbers or claiming evidence you did not observe does not satisfy it.

A complete submission can still state uncertainty. In fact, a bounded residual limitation is required. Examples include untested real-network cancellation behavior or browser scheduling variability; the limitation must be specific enough to prevent overclaiming.

References and freshness boundary

This lesson uses the frozen source set recorded in the lesson Bible:

  • S-MDN-ASYNC — MDN, async function: Promise-returning async functions and await control-flow semantics.
  • S-MDN-PROMISE — MDN, Promise: static concurrency methods and settlement semantics.
  • S-MDN-ABORT — MDN, AbortSignal: cancellation signalling and time-based signals.

The external claims are bounded to those source descriptions and were retrieved on 2026-09-02 with review due 2026-10-17. This lesson does not extend them into claims that async means parallel execution, that Promise.all is a limiter, that a timeout automatically cancels work, or that higher concurrency is universally faster or safer.

The evidence artifact itself is a MAKNOW instructional-design requirement rather than an external JavaScript fact. Browser/assistive-technology behavior is not claimed unless actually observed. When the review date arrives, or platform guidance materially changes, re-check the cited claims and narrow/update them before reusing the lesson as current guidance.

Assessment

There is no answer key in this lesson pack; answer it yourself and mark honestly. "Needs review" sends the lesson to your review queue.

WEB-003-S1 retrieval

State the core WEB-003 scheduling contract and one non-goal.

lesson ✗AI ✗notes ✗

WEB-003-S2 application

Implement a bounded scheduler for independent local work and record the configured limit, measured peak in-flight count, settlement behavior, and deadline/cancellation evidence.

lesson ✗AI ✗notes ✗

WEB-003-S3 diagnosis

Run one deliberate mutation that attacks concurrency budgeting or timeout/cancellation. Predict, observe, diagnose, repair, and show post-repair evidence.

lesson ✗AI ✗notes ✗

WEB-003-S4 decision

Defend bounded worker-pool scheduling against either immediate Promise.all fan-out or sequential await-in-loop for this scenario.

lesson ✗AI ✗notes ✗

WEB-003-S5 transfer

Solve the changed-context transfer fixture independently and produce a separate evidence block with recalculated budget/deadline expectations.

lesson ✗AI ✗notes ✗
Assessment rubric

Critical dimensions:

  1. Contract correctness — sequencing/concurrency choice, configured budget, timeout meaning and cancellation claim match the owned scenario.
  2. Evidence quality — measurements, mutation, diagnosis, repair and post-repair observations are traceable; appearance-only proof is insufficient.
  3. Scope discipline — TypeScript/module/bundler outcomes and unsupported production-performance claims are deferred.
  4. Transfer independence — changed-context work is solved without a decisive supplied implementation.

Blocking assessments: S2, S3, S5. S1 is retrieval-only and should not be failed solely for missing mutation/repair evidence when the stated contract and non-goal are correct. Owner acceptance is not fabricated by machine/local checks.

References & freshness

· TOOL_VOLATILE
An async function returns a Promise; await suspends that async function path and concurrent operations require explicit composition/error handling choices.
· TOOL_VOLATILE
Promise static concurrency methods aggregate already-supplied promises with different settlement semantics; they do not define an application resource budget.
· TOOL_VOLATILE
AbortSignal provides cancellation signalling and AbortSignal.timeout can create a signal that times out; the underlying operation must support/respond to the signal.

Case studies

Lab files

Purpose: prove a bounded scheduling contract with a deterministic local simulator. No network is required.

  1. Run python run_checks.py --self-test from the lab root. This checks fixture structure only; it does not certify your scheduler.
  2. Open learner/starter/index.html in a standards-capable browser, or use the Node-compatible core checks described below. Inspect scheduler-core.js before editing. Predict the baseline peak and timeout behavior.
  3. Repair mapWithConcurrency(items, limit, worker) so measured active work never exceeds limit while independent items can overlap.
  4. Repair runWithDeadline(taskFactory, timeoutMs) so an abort-aware task can receive a cancellation signal. Do not claim cancellation from a timer race alone.
  5. Run python mutation_probe.py --submission-dir learner/starter. Record at least one prediction/observation/diagnosis/repair cycle in the evidence file.
  6. Repair learner/transfer independently. Its item count, delays, failure placement and budget differ from the guided case.
  7. Complete learner/evidence/learner_evidence.json using the public artifact schema. Keep browser_dom_observation.claimed false until you have actually observed the fixture in a browser; when claimed, record the browser/environment, the direct observation, and the bounded inference separately.

A browser observation must be recorded only if you actually observed it. Node-compatible checks prove the scheduling core, not browser/AT behavior.

Lesson media & slides

MAKNOW_WEB-003_DECISION_EVIDENCE_TABLE_v1.0.0.svg
DECISION EVIDENCE TABLE v1.0.0.svg
MAKNOW_WEB-003_FAILURE_REPAIR_v1.0.0.svg
FAILURE REPAIR v1.0.0.svg
MAKNOW_WEB-003_PRIMARY_MODEL_v1.0.0.svg
PRIMARY MODEL v1.0.0.svg
MAKNOW_WEB-003_WORKED_EXAMPLE_v1.0.0.svg
WORKED EXAMPLE v1.0.0.svg

Teaching files (download)

XLSXAsync Failure Concurrency Budget Record v1 v1.0.0.xlsxTeaching files (download)
DOCXAsync Failure Concurrency Budget Record v1 WORKSHEET v1.0.0.docxTeaching files (download)
PDFAsync Failure Concurrency Budget Record v1 WORKSHEET v1.0.0.pdfTeaching files (download)