JavaScript Curriculum
Waiting Without Blocking
hardNexus needs to load a user profile from an API, then fetch their recent activity, then render the dashboard. Each step depends on the previous one. Each step takes time — the network is involved. You can't freeze the browser while you wait. JavaScript has a specific, elegant model for handling exactly this — and understanding it changes how you think about code.
JavaScript is single-threaded — but not single-tasked
JavaScript runs on a single thread. That means it can only do one thing at a time. If a synchronous operation takes 3 seconds — reading a file, waiting for a server, crunching a big dataset — the entire program freezes for 3 seconds. The browser can't respond to clicks. The UI locks up.
The browser and Node.js solve this with an architecture called the event loop. Long-running operations are offloaded to platform APIs (the browser's networking layer, the OS file system). JavaScript keeps running. When the operation completes, a callback is placed in a queue. The event loop moves it onto the call stack when the stack is clear.
Watch the full cycle with setTimeout:
call stack
empty
web apis
idle
task queue
empty
Code not yet running.
console output
nothing yet…
The three eras of async JavaScript
The same problem — fetch data, do something with it — has been solved three different ways as the language evolved. Understanding all three makes you a better debugger and a better code reader.
Era 1: Callbacks (pre-2015)
Pass a function to be called when the async work is done. Simple in concept, destructive at scale:
This is "callback hell" — each level of nesting pushes the logic rightward and duplicates error handling. Real codebases had six or seven levels. Code became impossible to follow.
Era 2: Promises (ES6, 2015)
A Promise is an object representing an operation that hasn't completed yet. It can be in one of three states — and it can only ever transition from pending to one of the settled states, never backwards:
Promise
pending.then() registers a handler for the resolved value. .catch() registers a handler for the rejection. They chain — each .then() can return a new Promise, and the chain continues:
One .catch() at the end handles errors from any stage. Flat, readable, far better.
Era 3: async/await (ES8, 2017)
async/await is syntactic sugar over Promises — it doesn't replace them, it wraps them in a syntax that reads like synchronous code. An async function always returns a Promise. await pauses execution of the async function until the awaited Promise settles — without blocking the thread.
The original async pattern. Pass a function to be called when the operation completes. Works, but nests badly — "callback hell" when multiple async operations chain together.
✗ Deep nesting, error handling repeated at every level, hard to reason about.
All four versions perform exactly the same operations — fetch user, then fetch posts, then log them. The difference is entirely in readability and error handling ergonomics.
The await keyword can only be used inside an async function. You cannot await at the top level of a module unless you're using ES2022 top-level await.
Error handling in async/await
Without try/catch, a rejected Promise will produce an unhandled rejection warning (and in Node.js, will crash the process). Always wrap await calls in try/catch:
Running operations in parallel
await is sequential by default — the second operation doesn't start until the first finishes. When two operations don't depend on each other, run them in parallel with Promise.all:
Promise.all takes an array of Promises and returns a single Promise that resolves when all of them resolve (with an array of their values), or rejects as soon as any one rejects.
The modern fetch pattern
fetch() — the browser's built-in HTTP function — returns a Promise. This is the pattern you'll write constantly in frontend JavaScript:
Your challenge
Wire up getUserDashboard using the two provided async helper functions. The structure is exactly the production pattern: await both operations, combine the results into an object, wrap everything in try/catch, return null on failure.
fetchUser and fetchActivity are provided — they simulate API calls with realistic delays. Your job is the orchestration layer around them.
Challenge
Write an async function called getUserDashboard(userId) that: (1) awaits a call to fetchUser(userId) — a provided async function that resolves to a user object, (2) awaits a call to fetchActivity(userId) — a provided async function that resolves to an array of activity strings, (3) returns an object with user and activity properties. Wrap everything in try/catch — on error, log the message and return null.
Waiting Without Blocking
hardNexus needs to load a user profile from an API, then fetch their recent activity, then render the dashboard. Each step depends on the previous one. Each step takes time — the network is involved. You can't freeze the browser while you wait. JavaScript has a specific, elegant model for handling exactly this — and understanding it changes how you think about code.
JavaScript is single-threaded — but not single-tasked
JavaScript runs on a single thread. That means it can only do one thing at a time. If a synchronous operation takes 3 seconds — reading a file, waiting for a server, crunching a big dataset — the entire program freezes for 3 seconds. The browser can't respond to clicks. The UI locks up.
The browser and Node.js solve this with an architecture called the event loop. Long-running operations are offloaded to platform APIs (the browser's networking layer, the OS file system). JavaScript keeps running. When the operation completes, a callback is placed in a queue. The event loop moves it onto the call stack when the stack is clear.
Watch the full cycle with setTimeout:
call stack
empty
web apis
idle
task queue
empty
Code not yet running.
console output
nothing yet…
The three eras of async JavaScript
The same problem — fetch data, do something with it — has been solved three different ways as the language evolved. Understanding all three makes you a better debugger and a better code reader.
Era 1: Callbacks (pre-2015)
Pass a function to be called when the async work is done. Simple in concept, destructive at scale:
This is "callback hell" — each level of nesting pushes the logic rightward and duplicates error handling. Real codebases had six or seven levels. Code became impossible to follow.
Era 2: Promises (ES6, 2015)
A Promise is an object representing an operation that hasn't completed yet. It can be in one of three states — and it can only ever transition from pending to one of the settled states, never backwards:
Promise
pending.then() registers a handler for the resolved value. .catch() registers a handler for the rejection. They chain — each .then() can return a new Promise, and the chain continues:
One .catch() at the end handles errors from any stage. Flat, readable, far better.
Era 3: async/await (ES8, 2017)
async/await is syntactic sugar over Promises — it doesn't replace them, it wraps them in a syntax that reads like synchronous code. An async function always returns a Promise. await pauses execution of the async function until the awaited Promise settles — without blocking the thread.
The original async pattern. Pass a function to be called when the operation completes. Works, but nests badly — "callback hell" when multiple async operations chain together.
✗ Deep nesting, error handling repeated at every level, hard to reason about.
All four versions perform exactly the same operations — fetch user, then fetch posts, then log them. The difference is entirely in readability and error handling ergonomics.
The await keyword can only be used inside an async function. You cannot await at the top level of a module unless you're using ES2022 top-level await.
Error handling in async/await
Without try/catch, a rejected Promise will produce an unhandled rejection warning (and in Node.js, will crash the process). Always wrap await calls in try/catch:
Running operations in parallel
await is sequential by default — the second operation doesn't start until the first finishes. When two operations don't depend on each other, run them in parallel with Promise.all:
Promise.all takes an array of Promises and returns a single Promise that resolves when all of them resolve (with an array of their values), or rejects as soon as any one rejects.
The modern fetch pattern
fetch() — the browser's built-in HTTP function — returns a Promise. This is the pattern you'll write constantly in frontend JavaScript:
Your challenge
Wire up getUserDashboard using the two provided async helper functions. The structure is exactly the production pattern: await both operations, combine the results into an object, wrap everything in try/catch, return null on failure.
fetchUser and fetchActivity are provided — they simulate API calls with realistic delays. Your job is the orchestration layer around them.
Challenge
Write an async function called getUserDashboard(userId) that: (1) awaits a call to fetchUser(userId) — a provided async function that resolves to a user object, (2) awaits a call to fetchActivity(userId) — a provided async function that resolves to an array of activity strings, (3) returns an object with user and activity properties. Wrap everything in try/catch — on error, log the message and return null.