Bachelors Program in Full Stack Development
Advanced JavaScript/TypeScript
•
Post Graduate
Asynchronous JavaScript and Concurrency
Introduction
Modern full-stack systems rarely perform all work sequentially. A web application may receive HTTP requests, query databases, read files, call external services, process streams, and update user interfaces at the same time. JavaScript is particularly interesting because its language execution model is predominantly single-threaded, while its host environments—browsers, Node.js runtimes, operating systems, and network stacks—provide extensive asynchronous capabilities.
Asynchrony and concurrency are related but not identical. Asynchrony concerns whether an operation completes independently of the current call stack. Concurrency concerns the management of multiple activities whose executions overlap in time. Parallelism is simultaneous execution on multiple processing units. JavaScript can coordinate concurrent activities without executing JavaScript instructions in parallel on one event-loop thread; workers and other mechanisms can provide actual parallelism.
This chapter develops a rigorous model of the event loop, callbacks, promises, async functions, cancellation, scheduling, failure, resource control, and concurrent design in JavaScript and TypeScript.
Learning Objectives
After studying this chapter, the reader should be able to:
Distinguish synchronous execution, asynchrony, concurrency, and parallelism.
Explain call stacks, queues, jobs, event loops, and host APIs.
Use callbacks, promises, and async/await correctly.
Analyse race conditions, ordering, backpressure, starvation, and failure propagation.
Apply TypeScript types to asynchronous APIs.
Select suitable concurrency strategies for browser and Node.js systems.
1. A Runtime Model of Asynchronous JavaScript
JavaScript language semantics define execution contexts, function calls, objects, promises, and jobs. They do not themselves define a browser’s networking stack or Node.js’s file system. The host environment supplies APIs such as fetch, timers, sockets, file operations, and worker creation.
A simplified execution model contains four important parts:
Call stack: the stack of currently executing JavaScript functions.
Heap: memory containing objects and closures.
Host facilities: subsystems that perform I/O, timers, and other operations outside the JavaScript stack.
Queues: scheduled callbacks and promise reactions waiting to execute.
When JavaScript invokes an asynchronous operation, the host may begin or register that operation and JavaScript can continue. When the operation settles, its continuation is placed in a queue. The event loop selects eligible work when the call stack is empty.
Figure: A simplified relationship among JavaScript execution, host facilities, queues, and the event loop.
Tasks and microtasks
A task—often called a macrotask—may include a timer callback, an input event, or an initial script. A microtask includes a promise reaction and a callback registered with queueMicrotask. After a task completes, the runtime generally drains the microtask queue before beginning another task or rendering opportunity. Consequently, this code prints A, C, then B:
A zero-millisecond timer does not mean “execute immediately.” It means “make this callback eligible after the minimum delay and scheduling rules have been satisfied.” Excessive microtasks can delay timers, input, and rendering; this is a form of starvation.
2. Callbacks, Promises, and Async Functions
A callback-based interface passes a function to be invoked later. Callbacks remain useful for event emitters and low-level APIs, but deeply nested callbacks can obscure control flow and make error propagation inconsistent.
A promise represents the eventual outcome of an operation. It has three conceptual states: pending, fulfilled, or rejected. Settlement is irreversible. A promise is not the result itself; it is a representation of a result that may become available later.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
delay(300)
.then(() => fetch("/api/profile"))
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.catch(error => console.error("Request failed", error));
Promise handlers are scheduled as microtasks. A handler that returns a value fulfills the next promise with that value. A handler that throws, or returns a rejected promise, rejects the next promise. This creates a compositional error channel.
async functions always return promises. Within an async function, await suspends that function’s continuation until the awaited promise settles; it does not block the event-loop thread.
async function loadProfile(signal?: AbortSignal): Promise<Profile> {
const response = await fetch("/api/profile", { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<Profile>;
}
The await expression is best understood as a boundary: the function returns control to its caller, and its remaining statements are scheduled later. Synchronous exceptions before an await become promise rejections, so callers still need try/catch or an equivalent policy.
3. Composition and Concurrency Combinators
Sequential await is appropriate when later work depends on earlier work or when rate limits require serialization. It is inefficient when independent operations can overlap.
Fulfills when all fulfill; rejects when the first rejects
All results are required
Promise.allSettled
Fulfills after every input settles
Independent outcomes must be reported
Promise.race
Settles when the first input settles
First-result or timeout patterns, with care
Promise.any
Fulfills on the first fulfillment; rejects if all reject
Redundant providers or replicas
Promise.all rejects early but does not automatically cancel remaining operations. If cancellation matters, combine it with AbortController or an application-specific cancellation mechanism.
This assumes that operation accepts the signal and that the underlying API honours it. A timeout that merely stops waiting, while the operation continues consuming resources, is not complete cancellation.
4. Concurrency Control and Backpressure
Launching one promise per item can overwhelm a database, remote service, browser connection pool, or memory budget. Concurrency must therefore be treated as a resource-management decision.
A simple worker-pool pattern limits active jobs:
async function mapLimit(items, limit, worker) {
const results = new Array(items.length);
let next = 0;
async function run() {
while (true) {
const index = next++;
if (index >= items.length) return;
results[index] = await worker(items[index], index);
}
}
const workers = Array.from(
{ length: Math.min(limit, items.length) },
run
);
await Promise.all(workers);
return results;
}
The result order is preserved because each result is stored at its original index, even though completion order may differ. The pattern assumes limit is a positive integer and that worker failures should reject the overall operation. Production implementations should define whether remaining work is cancelled after failure.
Backpressure occurs when producers generate work faster than consumers can process it. Useful strategies include bounded queues, pull-based iteration, stream high-water marks, admission control, batching, and rate limiting. Without a bound, queue length and memory consumption can grow indefinitely.
For a stable long-running system, average arrival rate must not exceed sustainable service capacity over the relevant interval. This is a capacity principle rather than a guarantee of low latency: bursts can still require buffering, shedding, or admission control.
5. Ordering, Races, and Shared State
A race condition occurs when the result depends on the timing or interleaving of operations. Consider a search box: a request for “ca” may return after a later request for “cat,” causing stale results to overwrite current results.
Two robust solutions are cancellation and generation checking:
let generation = 0;
let controller;
async function search(query) {
const current = ++generation;
controller?.abort();
controller = new AbortController();
try {
const response = await fetch(`/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal
});
const data = await response.json();
if (current === generation) render(data);
} catch (error) {
if (error.name !== "AbortError") report(error);
}
}
The generation check protects correctness even where cancellation is delayed or unsupported. In Node.js, shared mutable state across asynchronous callbacks can produce lost updates even without multiple JavaScript threads. A serialized queue, immutable state transition, database transaction, or mutex-like abstraction may be required.
Workers introduce actual parallel execution. They communicate through message passing and structured cloning, or through transferable objects and shared memory. SharedArrayBuffer and Atomics require especially careful reasoning about synchronization, visibility, and security; they should not be used merely because asynchronous APIs feel slow.
6. Errors, Cancellation, and Reliability
Asynchronous errors are not automatically caught by a surrounding synchronous try block if the asynchronous operation is not awaited within that block:
try {
fetch("/data");
} catch (e) {
// Does not catch a later rejection.
}
The promise must be awaited or explicitly handled. Every promise should have a deliberate ownership policy: the caller awaits it, a supervisory component observes it, or it is intentionally detached with logging and lifecycle control. Unhandled rejections can terminate processes or conceal failures depending on runtime configuration.
Reliable systems distinguish:
Transient failures: timeouts, temporary network errors, and overload; retries may help.
Permanent failures: invalid input, authentication failure, or unsupported requests; retries usually harm performance.
Cancellation: the caller no longer needs the result; it is not necessarily an error.
Retries should use bounded attempts, exponential backoff, and jitter. Retrying non-idempotent operations can duplicate side effects unless the API supports idempotency keys or transactional semantics. A retry policy must also respect server-provided rate-limit information.
Cleanup belongs in finally, including closing files, releasing locks, clearing timers, and removing event listeners. Cancellation should propagate through layers rather than being swallowed at an arbitrary boundary.
7. TypeScript Design for Asynchronous APIs
TypeScript expresses the value eventually produced by a promise:
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: Error };
async function safely<T>(task: () => Promise<T>): Promise<Result<T>> {
try {
return { ok: true, value: await task() };
} catch (cause) {
const error = cause instanceof Error ? cause : new Error(String(cause));
return { ok: false, error };
}
}
This result type makes failure explicit and supports exhaustive handling. It is useful at domain boundaries, although exceptions may remain more ergonomic for internal control flow.
Generic interfaces can specify cancellation and progress:
Types do not prove that an operation is race-free, cancellable, idempotent, or free from resource leaks. Documentation and tests must state these behavioural contracts.
8. Browser and Node.js Contexts
Browsers coordinate rendering, user input, networking, timers, and workers. Long synchronous computation blocks input and rendering, so CPU-heavy work should be divided into yielding chunks or moved to a Web Worker. requestAnimationFrame is appropriate for visual updates, whereas timers are not precise frame schedulers.
Node.js uses an event-driven architecture for networking and many I/O operations. Synchronous file-system or cryptographic calls can block the event loop and reduce throughput. The libuv implementation may use a thread pool for selected operations, but this does not make arbitrary JavaScript code parallel. Worker threads are suitable for CPU-intensive JavaScript computations.
Streams provide incremental processing and backpressure. A pipeline that reads an entire large file into memory before sending it is simpler but less scalable than a streaming design. The appropriate choice depends on payload size, latency, error recovery, and operational complexity.
9. Applied Case: Inventory Reservation Service
This is an illustrative case.
Background
An online retailer receives checkout requests through several application instances. Each request validates payment, reserves inventory, and publishes an order event.
Problem
During a promotion, multiple requests for the last item arrive nearly simultaneously. A naïve implementation reads stock, waits for payment, and then writes a decremented quantity. Concurrent requests can both observe the same stock value. External payment calls also have variable latency, and clients may retry after timeouts.
Analysis
The problem combines a race condition, an external side effect, uncertain completion, and retry duplication. Increasing JavaScript concurrency does not solve the consistency problem. It may amplify it.
Approach
The service uses an atomic database operation such as “decrement only when quantity is positive,” or a transaction with an appropriate isolation strategy. Each checkout carries an idempotency key. Payment and reservation have explicit state transitions. The request handler uses an abort signal for client disconnects, but cancellation is not treated as proof that a remote payment did not complete. A durable outbox records the order event in the same transaction as the reservation; a separate publisher retries delivery.
At the application layer, requests are bounded by a concurrency limit, and downstream failures use carefully selected retry policies. Metrics record latency, active operations, rejection causes, queue depth, and idempotency conflicts.
Outcome
Overselling is prevented by the database invariant rather than by timing assumptions. Duplicate client requests resolve to the original outcome. Temporary message-broker failures do not lose events, and bounded concurrency prevents the promotion from exhausting service resources.
Learning Points
Promise concurrency is not a substitute for transactional integrity.
Cancellation, timeout, and failure are different states.
Idempotency is essential when clients or networks retry.
Backpressure and observability are reliability features, not optional optimizations.
10. Testing, Observability, and Best Practices
Asynchronous tests should await the operation under test and use deterministic clocks or controlled schedulers where possible. Tests should cover success, rejection, timeout, cancellation, partial failure, duplicate invocation, ordering, and resource cleanup. Avoid arbitrary sleeps as synchronization; they create slow and flaky tests.
Useful observability signals include operation duration, queue wait time, active concurrency, retry count, cancellation count, error category, and event-loop delay. Distributed tracing should propagate correlation identifiers through asynchronous boundaries while avoiding sensitive data in logs.
Recommended practices are:
Make sequencing explicit; do not depend on accidental timing.
Use Promise.all only when all operations may safely overlap.
Bound concurrency and memory growth.
Propagate cancellation and clean up resources.
Preserve error causes and classify failures.
Avoid blocking the event loop with CPU-heavy or synchronous work.
Design retries around idempotency and service contracts.
Treat unhandled rejections as defects.
Document ordering, cancellation, and failure semantics in TypeScript APIs.
Measure before optimizing; more concurrency can increase contention and latency.
Contemporary Developments and Critical Perspective
Modern JavaScript runtimes increasingly support structured cancellation through AbortSignal, asynchronous iterators, Web Streams, and worker-based computation. Frameworks may add scheduling priorities, server rendering, and streaming responses. These abstractions improve productivity but can obscure ownership: developers must still identify who starts an operation, who cancels it, who observes its failure, and who releases its resources.
AI-enabled applications intensify these concerns. Streaming model responses, tool calls, vector searches, and moderation checks often form a concurrent workflow. Systems require deadlines, bounded parallel tool calls, cancellation when a user disconnects, protection against duplicated side effects, and careful handling of confidential prompts and outputs. Governance requires auditability without indiscriminate retention of sensitive data.
Chapter Summary
Asynchronous JavaScript separates the progress of an operation from the current call stack. The event loop coordinates JavaScript with host-provided I/O, tasks, and microtasks. Promises and async functions provide compositional control flow, but they do not automatically provide cancellation, ordering, resource bounds, or parallelism. Promise combinators express different completion policies; worker pools and backpressure control resource use; generation checks, cancellation, transactions, and idempotency address races and repeated effects. Reliable systems classify errors, propagate cancellation, clean up in finally, test timing-sensitive behaviour, and observe queueing and failure. TypeScript can document asynchronous value types and result contracts, but behavioural correctness still depends on architecture, runtime semantics, and operational discipline.
Key Terms
Asynchrony: Progress that is not completed within the current synchronous call stack.
Concurrency: Overlapping management or execution of multiple activities.
Parallelism: Simultaneous execution on multiple processing units.
Call stack: Runtime structure containing currently executing function contexts.
Event loop: Scheduler that selects eligible asynchronous work for execution.
Task: A scheduled unit such as a timer or input callback.
Microtask: High-priority queued work such as a promise reaction.
Promise: Object representing a future fulfillment or rejection.
Async function: Function that returns a promise and may suspend at await.
Cancellation: Signalling that an operation is no longer required.
Backpressure: Control that prevents a producer from overwhelming a consumer.
Race condition: Timing-dependent behaviour arising from competing operations.
Idempotency: Property that repeating an operation has the intended single effect.
Worker pool: A bounded set of concurrent workers processing a workload.
Starvation: Indefinite postponement caused by competing work.
Structured cloning: Runtime mechanism for copying values across worker or message boundaries.
AbortSignal: Signal used to communicate cancellation to compatible APIs.
Outbox pattern: Durable recording of an event for later reliable publication.
Review Questions
5 Short Answer Questions
Distinguish asynchrony from parallelism.
What is the relationship between a promise and an async function?
Why can a zero-delay timer execute after a promise reaction?
What problem does backpressure address?
Why is cancellation not equivalent to failure?
5 Long Answer Questions
Explain the call stack, host APIs, task queue, microtask queue, and event loop as a complete runtime model.
Compare Promise.all, Promise.allSettled, Promise.race, and Promise.any, including appropriate use cases.
Analyse how unbounded concurrency can affect latency, memory, and downstream services.
Explain how TypeScript can express asynchronous value and error contracts, and identify what it cannot prove.
Discuss why retries, idempotency, cancellation, and transactional integrity must be designed together.
3 Analytical / Application Questions
Design a bounded and cancellable workflow that downloads 10,000 files while preserving result order and limiting active downloads to 20.
A search interface displays stale results when users type rapidly. Analyse the race and propose two independent correctness mechanisms.
Evaluate the inventory-reservation case: identify which guarantees belong in JavaScript, which belong in the database, and which belong in messaging infrastructure.
Multiple Choice Questions with Answers
Question 1
Which statement best distinguishes concurrency from parallelism?
A. Concurrency always requires multiple CPU cores.
B. Concurrency manages overlapping activities, while parallelism executes work simultaneously on multiple processing units.
C. Parallelism is limited to browser timers.
D. Concurrency means that all operations finish in submission order.
Correct Answer: B. Concurrency manages overlapping activities, while parallelism executes work simultaneously on multiple processing units.
Question 2
What normally happens to promise reactions after a task completes?
A. They are discarded until the next page load.
B. They are placed in the microtask queue and processed before the next task opportunity.
C. They are converted into worker threads.
D. They execute synchronously inside the completed task.
Correct Answer: B. They are placed in the microtask queue and processed before the next task opportunity.
Question 3
Which combinator waits for every input promise to settle and reports both fulfilled and rejected outcomes?
A. Promise.all
B. Promise.race
C. Promise.allSettled
D. Promise.any
Correct Answer: C. Promise.allSettled
Question 4
Why is sequential await sometimes less efficient than Promise.all?
A. Sequential await disables error handling.
B. Sequential await cannot return values.
C. Sequential await prevents independent operations from overlapping.
D. Sequential await always creates worker threads.
Correct Answer: C. Sequential await prevents independent operations from overlapping.
Question 5
What is the principal purpose of a concurrency limit?
A. To guarantee that every operation succeeds.
B. To bound resource use and protect downstream systems from excessive simultaneous work.
C. To make all operations execute in parallel.
D. To eliminate the need for cancellation.
Correct Answer: B. To bound resource use and protect downstream systems from excessive simultaneous work.
Question 6
Which technique helps prevent an older search response from overwriting newer results?
A. Ignoring all rejected promises.
B. Using a generation check or cancelling the previous request.
C. Adding a zero-millisecond timer to every response.
D. Executing every request synchronously.
Correct Answer: B. Using a generation check or cancelling the previous request.
Question 7
What does AbortController provide when used with a compatible fetch operation?
A. A guarantee that the remote server has undone its side effect.
B. A signal that can request cancellation of the operation.
C. Automatic database rollback.
D. Parallel execution of the fetch callback.
Correct Answer: B. A signal that can request cancellation of the operation.
Question 8
Why can retrying a non-idempotent operation be dangerous?
A. Retries always reduce throughput to zero.
B. The repeated attempt can duplicate an external side effect.
C. Non-idempotent operations cannot return promises.
D. Retries prevent all network failures.
Correct Answer: B. The repeated attempt can duplicate an external side effect.
Question 9
Which mechanism is most appropriate for CPU-intensive JavaScript work that would otherwise block the event loop?
A. A longer synchronous loop on the main thread.
B. A microtask scheduled recursively.
C. A worker thread or Web Worker.
D. A zero-delay timer without yielding computation.
Correct Answer: C. A worker thread or Web Worker.
Question 10
What is the main reliability role of an outbox pattern?
A. It ensures events are durably recorded for later publication alongside a state change.
B. It forces all network calls to be synchronous.
C. It removes the need for database transactions.
D. It guarantees that consumers never receive duplicate messages.
Correct Answer: A. It ensures events are durably recorded for later publication alongside a state change.