0% found this document useful (0 votes)
62 views7 pages

Asynchronous JavaScript Deep Dive

Notes on Async js
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
62 views7 pages

Asynchronous JavaScript Deep Dive

Notes on Async js
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 7

synchronous JavaScript lets long-running tasks (like network requests, timers, and disk I/O)

run without blocking the single-threaded main thread, keeping apps responsive while work
completes in the background.

Core idea

• JavaScript runs on a single thread with a call stack, so blocking operations would
freeze UI or delay other code if executed synchronously.

• Asynchronous APIs schedule callbacks to run later via the event loop, which pulls
work from queues (tasks/macrotasks and microtasks) when the call stack becomes
empty.

Event loop, tasks, microtasks

• A “task” (macrotask) is a unit like a timer, user input, or network callback; after a task
completes, the engine drains the microtask queue before running the next task.

• Microtasks (e.g., promise reactions) have higher priority than macrotasks; they run to
completion after each task, before any new task is selected.

• Ordering example: synchronous logs run first, then promise .then microtasks, then
setTimeout callbacks (even with 0 ms), producing “Start → End → Promise →
Timeout”.

Callback pattern

• Early async JS used callbacks passed into APIs such as setTimeout(fn, ms) or XHR;
callbacks execute later when the event loop schedules them.

• Callbacks can nest deeply (“callback hell”), making control flow and error handling
difficult to reason about for complex sequences.

Example — callback-based timer:

js

console.log("A");

setTimeout(() => {

console.log("B after 0ms");

}, 0);

console.log("C");

Expected order: A, C, then B, because the timeout callback is a macrotask scheduled after
current code and any pending microtasks.

Promises
• A Promise is a proxy for a value that may not be known yet, representing states:
pending → fulfilled or rejected; handlers attach via then/catch/finally and run when
settled.

• Promises flatten nested callbacks and enable composition (chaining and concurrency
helpers like all, race, allSettled, any) for clearer async control flow.

Example — wrapping a callback API:

js

function delay(ms) {

return new Promise((resolve) => setTimeout(resolve, ms));

delay(500).then(() => console.log("Half a second passed"));

The Promise executor schedules resolution via setTimeout, and the .then handler runs as a
microtask after the resolving task completes.

Promise chaining

• Returning a value in then passes it to the next then; returning a promise in then waits
for that promise, enabling sequential async steps without nesting.

• Errors thrown in then or rejections propagate to the nearest catch, centralizing error
handling for chains.

Example — sequential steps:

js

fetch("/api/user")

.then(r => {

if (!r.ok) throw new Error(r.status);

return r.json();

})

.then(user => fetch(`/api/projects?user=${user.id}`))

.then(r => r.json())

.then(projects => console.log(projects))

.catch(err => console.error("Failed:", err));


This uses promise composition to run HTTP requests in sequence with a single catch at the
end for any failure in the chain.

Concurrency with Promise.all

• Promise.all runs multiple promises in parallel and fulfills with an array when all
succeed, or rejects immediately if any input promise rejects.

• Use when tasks are independent but results are needed together, improving total
latency by starting them simultaneously.

Example — parallel fetches:

js

const urls = ["/a.json", "/b.json", "/c.json"];

Promise.all(urls.map(u => fetch(u).then(r => r.json())))

.then(([a, b, c]) => console.log(a, b, c))

.catch(err => console.error("One failed:", err));

This fires all requests at once and aggregates results in the original order upon fulfillment, or
bails on first rejection.

async/await

• Marking a function async makes it return a promise; within it, await pauses the
function until the awaited promise settles, resuming with its value or throwing its
error.

• async/await makes promise code read like synchronous code while preserving non-
blocking behavior, with try/catch for straightforward error handling.

Example — sequential with await:

js

async function loadUserProjects() {

const r1 = await fetch("/api/user");

if (!r1.ok) throw new Error(r1.status);

const user = await r1.json();

const r2 = await fetch(`/api/projects?user=${user.id}`);

if (!r2.ok) throw new Error(r2.status);


const projects = await r2.json();

return projects;

loadUserProjects().then(console.log).catch(console.error);

The function yields at each await without blocking the event loop, and returns a promise
that fulfills with projects or rejects on error.

Parallel with await

• For independent operations, start them first, then await both, avoiding serial waits
that waste time.

• This pattern mirrors Promise.all ergonomically while keeping explicit variables for
readability and error grouping if needed.

Example — start then await:

js

async function loadAll() {

const aP = fetch("/a.json");

const bP = fetch("/b.json");

const [a, b] = await Promise.all([aP, bP]);

return Promise.all([a.json(), b.json()]);

Both network requests run in parallel; Promise.all coordinates fulfillment and propagates
any single rejection.

Microtasks vs macrotasks in practice

• Promise callbacks (.then/.catch/finally) queue microtasks, which run before timers or


I/O callbacks, affecting observable ordering in logs and UI updates.

• queueMicrotask allows scheduling a microtask directly, useful for post-state-update


reactions that must run before the next task.

Example — ordering demo:

js
console.log("start");

setTimeout(() => console.log("timeout 0"), 0);

Promise.resolve().then(() => console.log("microtask"));

queueMicrotask(() => console.log("queued microtask"));

console.log("end");

Order: start, end, microtask, queued microtask, timeout 0, because microtasks flush before
the next macrotask.

Structured async iteration

• for await...of iterates over async iterables (e.g., streams, paginated APIs), awaiting
each produced value, simplifying consumption of asynchronous data sources.

• This pattern aligns with fetch streaming and Node.js async generators, offering
backpressure-friendly iteration with clear syntax.

Example — async generator and consumption:

js

async function* paginate(url) {

let next = url;

while (next) {

const r = await fetch(next);

if (!r.ok) throw new Error(r.status);

const { items, nextPage } = await r.json();

yield items;

next = nextPage;

for await (const page of paginate("/api/items?page=1")) {

console.log("got page", page.length);

}
Each await pauses within the loop without blocking the thread, fetching pages lazily as
needed.

Error handling patterns

• Try/catch inside async functions catches awaited rejections; outside, always attach a
catch to the returned promise to avoid unhandled rejections.

• For multiple parallel awaits, wrap with Promise.all and catch once, or handle results
individually using Promise.allSettled to inspect each outcome safely.

Example — allSettled:

js

const results = await Promise.allSettled(urls.map(u => fetch(u)));

for (const r of results) {

if (r.status === "fulfilled") console.log("OK");

else console.warn("Failed:", r.reason);

allSettled never short-circuits, returning an array of outcome objects for robust bulk
operations.

Practical guidelines

• Prefer promises/async-await over raw callbacks for readability, composability, and


error handling, except when interfacing with legacy APIs that force callbacks.

• Use concurrency deliberately: parallelize independent work, but sequence


dependent steps; keep in mind microtask priority when orchestrating UI updates and
logs.

Mini-exercises

• Rewrite a nested callback sequence (timer → fetch → parse) into promise chains,
then into async/await, verifying identical behavior with console logs to observe
ordering.

• Add both Promise.resolve().then(...) and setTimeout(..., 0) to a snippet to predict and


confirm the execution order based on microtask vs macrotask queues.

Advanced notes

• The execution model specifies job queues and priorities; different environments
(browsers vs Node) may vary in task categorization, but microtasks still run before
the next task.
• Newer syntax like await using and async disposables integrate cleanup patterns with
async code, improving resource safety across awaits and loops.

Key references for deeper study: MDN’s Async JS module, Promise guides, async function
reference, execution model, microtask guides, and Promise combinators

You might also like