Day 15-17: Basic Async Patterns
🎯 Learning Objectives
-
By the end of this day, you will be able to explain the roles of
asyncandawaitin managing asynchronous operations. -
By the end of this day, you will be able to write asynchronous
functions that pause execution using
awaituntil a Promise resolves. - By the end of this day, you will be able to implement sequential asynchronous workflows by awaiting Promises one after another.
-
By the end of this day, you will be able to utilize
Promise.allwithawaitto execute multiple independent asynchronous operations in parallel.
📚 Concept Introduction: Why This Matters
Paragraph 1 - The Problem: Before modern asynchronous
patterns, JavaScript developers wrestled with a problem known as
"Callback Hell." To perform a sequence of asynchronous actions—like
fetching user data, then their posts, then their comments—you had to
nest functions within functions. This created a "pyramid of doom," a
deeply indented block of code that was incredibly difficult to read,
reason about, and debug. Error handling was particularly nightmarish,
requiring separate error callbacks at each level of nesting. While
Promises improved this by allowing .then() chaining,
creating a flatter structure, long chains could still become verbose
and managing complex conditional logic within them remained
cumbersome. The fundamental challenge was that asynchronous code
didn't look or behave like the rest of the synchronous language,
creating a jarring mental context switch for developers.
Paragraph 2 - The Solution: The introduction of
async/await syntax in ES2017 was a revolutionary solution
to this long-standing problem. It provides a layer of syntactic sugar
on top of Promises, allowing developers to write asynchronous code
that looks and feels synchronous. The async keyword
declares that a function will handle asynchronous operations, and the
await keyword pauses the function's execution at a
specific line until a Promise settles (either resolves with a value or
rejects with an error). This completely eliminates the need for nested
callbacks or long .then() chains for sequential
operations. Instead of nesting, you simply write each asynchronous
call on a new line, just as you would with normal synchronous code,
making the logic flow linear and intuitive.
Paragraph 3 - Production Impact: In professional
codebases, async/await is the undisputed standard for
managing asynchronous operations. Its adoption is nearly universal
because of its dramatic impact on code quality and developer
productivity. The primary benefit is a massive increase in readability
and maintainability; code is simply easier to understand at a glance.
Error handling becomes trivial using standard
try...catch blocks, a familiar pattern from synchronous
programming, which centralizes error logic. Furthermore, debugging is
significantly improved because the call stack is preserved across
await calls, making it easier to trace the origin of an
error. For teams, this means faster onboarding for new developers,
fewer bugs related to complex asynchronous logic, and quicker
development cycles for features that rely on APIs, databases, or other
asynchronous resources.
🔍 Deep Dive: await EXPRESSION
Pattern Syntax & Anatomy
// The `async` keyword is required to use `await` inside a function.
async function fetchData() {
// ↑ Keyword marking the function as asynchronous. It implicitly returns a Promise.
try {
// `await` pauses the function until the Promise from `fetch` resolves.
const response = await fetch('https://api.example.com/data');
// ↑ The await keyword. Pauses execution.
// ↑ An expression that returns a Promise.
// This line only runs AFTER the `await` above is complete.
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to fetch data:", error);
}
}
How It Actually Works: Execution Trace
Let's trace exactly what happens when fetchData() is
called:
Step 1: The fetchData function is called. Because it is
marked async, JavaScript immediately returns a Promise in
a pending state. The code inside the function begins to
execute.
Step 2: The try...catch block is entered. The first line
const response = await fetch(...) is encountered. The
fetch function is called, which initiates an HTTP network
request and returns a Promise for the future
Response object.
Step 3: The await keyword sees the Promise returned by
fetch. It does two things: it "unwraps" the future value
from the Promise and, most importantly, it pauses the execution of the
fetchData function right at this line. It yields control
back to the JavaScript event loop.
Step 4: While fetchData is paused, the JavaScript engine
is not blocked. It is free to execute other code, handle user
interactions, or run other asynchronous tasks. The network request for
the data happens in the background (managed by the browser or Node.js
runtime).
Step 5: Eventually, the network request completes. The Promise
returned by fetch resolves with the
Response object. The event loop sees that the awaited
Promise is done and schedules the fetchData function to
resume execution.
Step 6: The function "wakes up" where it left off. The resolved
Response object is assigned to the
response constant. Execution continues to the next line,
const data = await response.json(). This process repeats:
response.json() returns a Promise,
await pauses the function again, and it resumes once the
JSON body is parsed and the Promise resolves, assigning the result to
data. Finally, the data is returned, which
resolves the original Promise returned by fetchData.
Example Set (REQUIRED: 6 Complete Examples)
Example 1: Foundation - Simplest Possible Usage
// A simple function that simulates fetching a user's name after a delay.
function fetchUserName() {
// We return a Promise that resolves after 1 second.
return new Promise(resolve => {
setTimeout(() => {
resolve('Alice');
}, 1000);
});
}
// An async function is needed to use the 'await' keyword.
async function displayUser() {
console.log('Fetching user...');
const userName = await fetchUserName(); // Pause execution here for 1 second
console.log(`User found: ${userName}`);
}
displayUser();
// Expected output:
// Fetching user...
// (after 1 second)
// User found: Alice
This example shows the most fundamental use of
async/await. The displayUser function pauses
at the await call and only continues to the final
console.log after the fetchUserName Promise
has successfully resolved.
Example 2: Practical Application
// Real-world scenario: Fetching post data and then the author's details.
async function getPostDetails(postId) {
try {
console.log(`Fetching post ${postId}...`);
// First, get the main post data
const postResponse = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
const post = await postResponse.json();
console.log(`Fetching author ${post.userId}...`);
// Then, use the userId from the post to get the author's data
const userResponse = await fetch(`https://jsonplaceholder.typicode.com/users/${post.userId}`);
const user = await userResponse.json();
// Combine the data
const postDetails = {
...post,
author: user.name
};
console.log(postDetails);
return postDetails;
} catch (error) {
console.error('Error fetching post details:', error);
}
}
getPostDetails(1);
This demonstrates a common real-world pattern: a sequential chain of
asynchronous operations. We must wait for the first fetch to complete
to get the userId before we can initiate the second
fetch, making sequential await calls the perfect tool.
Example 3: Handling Edge Cases
// What happens when the awaited Promise rejects (e.g., a network error)?
async function fetchNonExistentResource() {
const apiUrl = 'https://jsonplaceholder.typicode.com/posts/999999';
console.log(`Attempting to fetch from: ${apiUrl}`);
try {
const response = await fetch(apiUrl);
// fetch() only rejects on network errors, not HTTP error codes.
// So we must check the response status ourselves.
if (!response.ok) {
// Create a custom error to be caught by the catch block.
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log('Success:', data);
} catch (error) {
// The catch block gracefully handles both network failures and our custom error.
console.error('Caught an error!');
console.error('Details:', error.message);
} finally {
console.log('Fetch attempt finished.');
}
}
fetchNonExistentResource();
This highlights the critical importance of
try...catch for error handling with
async/await. Unlike .then().catch(), this
allows us to use a single, familiar block to handle any failure that
occurs within the try block, including network errors or
manual throw statements for bad HTTP statuses.
Example 4: Pattern Combination
// Combining `await` with `Promise.all` for parallel execution
async function fetchDashboardData() {
console.log('Fetching dashboard data...');
const userPromise = fetch('https://jsonplaceholder.typicode.com/users/1');
const postsPromise = fetch('https://jsonplaceholder.typicode.com/posts?userId=1');
const commentsPromise = fetch('https://jsonplaceholder.typicode.com/comments?postId=1');
// Promise.all takes an array of promises and returns a single promise
// that resolves when all of the input promises have resolved.
try {
const [userResponse, postsResponse, commentsResponse] = await Promise.all([
userPromise,
postsPromise,
commentsPromise
]);
// Now, process the responses (also an async operation)
const user = userResponse.json();
const posts = postsResponse.json();
const comments = commentsResponse.json();
// Await another Promise.all for the JSON parsing
const [userData, postsData, commentsData] = await Promise.all([user, posts, comments]);
console.log(`Welcome, ${userData.name}`);
console.log(`You have ${postsData.length} posts.`);
console.log(`The first post has ${commentsData.length} comments.`);
} catch(error) {
console.error("Failed to fetch all dashboard data", error);
}
}
fetchDashboardData();
This is a crucial performance pattern. Instead of awaiting each fetch
sequentially, we initiate all three network requests at once. We then
use a single await Promise.all() to pause until all three
have completed, significantly reducing the total wait time.
Example 5: Advanced/Realistic Usage
// Production-level implementation: A generic fetch utility with retries.
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function fetchWithRetry(url, options = {}, retries = 3, backoff = 300) {
let attempt = 1;
while (attempt <= retries) {
try {
console.log(`Attempt ${attempt} to fetch ${url}...`);
const response = await fetch(url, options);
if (!response.ok) {
// Throw an error for bad responses to trigger the catch block.
throw new Error(`Request failed with status ${response.status}`);
}
// If successful, return the data and exit the loop.
return await response.json();
} catch (error) {
console.warn(`Attempt ${attempt} failed: ${error.message}`);
if (attempt === retries) {
// If this was the last retry, re-throw the error.
console.error("All retry attempts failed.");
throw error;
}
// Wait for a backoff period before the next attempt.
await sleep(backoff * attempt);
attempt++;
}
}
}
// Usage of the utility function
async function main() {
// Example with a URL that will fail
const badUrl = 'https://jsonplaceholder.typicode.com/invalid-url';
await fetchWithRetry(badUrl).catch(err => { /* Gracefully handle final failure */ });
}
main();
This example shows a professional-grade, reusable utility. It combines
await within a while loop,
try...catch for error handling, and a delay mechanism
(sleep) to create a robust function that can recover from
transient network failures, a common requirement in production
applications.
Example 6: Anti-Pattern vs. Correct Pattern
// ❌ ANTI-PATTERN - Unnecessary .then() chaining inside an async function
async function getPostsBad() {
console.log('Fetching posts (bad way)...');
// This mixes two different styles and defeats the purpose of async/await's clarity.
return fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => {
// It works, but it's more verbose and harder to read.
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(posts => {
console.log(`Found ${posts.length} posts.`);
return posts.slice(0, 5); // Return first 5 posts
})
.catch(error => {
console.error('An error occurred:', error);
return []; // Return an empty array on failure
});
}
// âś… CORRECT APPROACH - Using await for clean, linear code
async function getPostsGood() {
console.log('Fetching posts (good way)...');
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const posts = await response.json();
console.log(`Found ${posts.length} posts.`);
return posts.slice(0, 5); // Return first 5 posts
} catch (error) {
console.error('An error occurred:', error);
return []; // Return an empty array on failure
}
}
getPostsGood();
The anti-pattern mixes async/await with
then/catch chains, nullifying the readability benefits of
the modern syntax. The correct approach fully embraces
async/await and try...catch, resulting in
code that is linear, easier to debug, and requires less cognitive
overhead to understand. Always prefer the pure
async/await style within an async function.
⚠️ Common Pitfalls & Solutions
Pitfall #1: Forgetting the await Keyword
What Goes Wrong: A very common mistake for developers
new to async/await is to call an
async function (or any function that returns a Promise)
but forget to put the await keyword in front of it. When
this happens, you don't get the resolved value you expect. Instead,
you get the Promise object itself, which is almost never what you
want.
Your code will continue to execute immediately without pausing,
leading to race conditions and bugs. For example, if you try to access
a property on the result (e.g., result.name), you'll be
trying to access it on a pending Promise object, which will result in
undefined. This can lead to subtle bugs that are hard to
track down because no explicit error is thrown at the point of the
missing await.
Code That Breaks:
async function getUser() {
// Simulates an API call
return new Promise(resolve => setTimeout(() => resolve({ id: 1, name: 'John Doe' }), 500));
}
async function displayGreeting() {
console.log('Fetching user...');
const user = getUser(); // ❌ MISTAKE: `await` is missing!
// This line runs immediately, before the user data has been fetched.
// `user` is a pending Promise, not the user object.
console.log(`Hello, ${user.name}!`); // Outputs: "Hello, undefined!"
console.log('User object is actually:', user); // Outputs: "User object is actually: Promise { <pending> }"
}
displayGreeting();
Why This Happens: An async function, by
definition, always returns a Promise. The await keyword
is the mechanism that "unpacks" the resolved value from that Promise.
If you omit await, you are simply holding onto the
Promise object itself. The rest of your function's code does not pause
and continues executing synchronously.
The Fix:
async function getUser() {
return new Promise(resolve => setTimeout(() => resolve({ id: 1, name: 'John Doe' }), 500));
}
async function displayGreetingFixed() {
console.log('Fetching user...');
// âś… FIX: Add the `await` keyword to pause and get the resolved value.
const user = await getUser();
// This line now waits until the promise resolves.
console.log(`Hello, ${user.name}!`); // Outputs: "Hello, John Doe!"
console.log('User object is now:', user); // Outputs the actual user object
}
displayGreetingFixed();
Prevention Strategy: Use a linter like ESLint with
modern configurations. Many rulesets (like
eslint-plugin-promise) have rules that can detect when a
Promise-returning function is called without being
awaited or having a .then() attached.
Furthermore, whenever you are working with a function you know is
asynchronous, make it a mental habit to ask, "Do I need the result of
this operation before I continue?" If the answer is yes, you almost
certainly need to use await.
Pitfall #2: Creating Unintentional Waterfalls
What Goes Wrong: await is so convenient
for writing sequential code that it's easy to overuse it and
accidentally make your code much slower than it needs to be. This
happens when you have multiple asynchronous operations that
do not depend on each other, but you await them
one by one. Each await pauses execution, so the second
operation doesn't even start until the first one is completely
finished.
This creates a "waterfall" where the total execution time is the sum of all the individual operation times. If the operations could have been run concurrently, you have introduced a significant and unnecessary performance bottleneck. For example, fetching a user's profile and their recent notifications from two different API endpoints are likely independent tasks that could be done at the same time.
Code That Breaks:
// Simulating API calls with delays
const fetchProfile = () => new Promise(res => setTimeout(() => res({ user: 'Jane' }), 1000));
const fetchPosts = () => new Promise(res => setTimeout(() => res({ posts: ['Post 1'] }), 1500));
const fetchFriends = () => new Promise(res => setTimeout(() => res({ friends: ['Bob'] }), 1200));
async function loadDashboardSequentially() {
console.time('SequentialLoad');
console.log('Loading dashboard data...');
// ❌ ANTI-PATTERN: These do not depend on each other, but are awaited in sequence.
const profile = await fetchProfile(); // Waits ~1000ms
const posts = await fetchPosts(); // Starts after 1000ms, waits ~1500ms
const friends = await fetchFriends(); // Starts after 2500ms, waits ~1200ms
console.log('Dashboard loaded!');
console.timeEnd('SequentialLoad'); // Total time will be ~3700ms (1000 + 1500 + 1200)
}
loadDashboardSequentially();
Why This Happens: The await keyword
explicitly tells JavaScript to halt the function's progress until the
awaited Promise is resolved. When you chain them like this, you are
building a strict sequence of operations, even if the logic doesn't
require it. You force fetchPosts to wait for
fetchProfile to finish, which is inefficient.
The Fix:
const fetchProfile = () => new Promise(res => setTimeout(() => res({ user: 'Jane' }), 1000));
const fetchPosts = () => new Promise(res => setTimeout(() => res({ posts: ['Post 1'] }), 1500));
const fetchFriends = () => new Promise(res => setTimeout(() => res({ friends: ['Bob'] }), 1200));
async function loadDashboardInParallel() {
console.time('ParallelLoad');
console.log('Loading dashboard data...');
// âś… FIX: Initiate all requests at once.
const profilePromise = fetchProfile();
const postsPromise = fetchPosts();
const friendsPromise = fetchFriends();
// Wait for all of them to complete in parallel using Promise.all.
const [profile, posts, friends] = await Promise.all([
profilePromise,
postsPromise,
friendsPromise
]);
console.log('Dashboard loaded!');
console.timeEnd('ParallelLoad'); // Total time will be ~1500ms (the duration of the longest request)
}
loadDashboardInParallel();
Prevention Strategy: Before writing a series of
await calls, always ask: "Does the input of step 2 depend
on the output of step 1?" If the answer is no, the operations are
independent and should be run in parallel. Initiate all the
promise-returning functions first, store their promises in variables,
and then use await Promise.all() to wait for all of them
to complete. This habit will ensure you are writing performant
asynchronous code by default.
Pitfall #3: await Inside Non-async
Loops
What Goes Wrong: A common point of confusion is how
await behaves inside loops. Using
await within standard synchronous loop methods like
forEach() does not work as you might expect. The callback
function passed to forEach is a separate, synchronous
function. If you mark it as async, each call creates a
new Promise, but the forEach loop itself does not
await them. It will blaze through all the iterations,
firing off all the promises, and your parent function will continue
executing without waiting for any of them to finish.
This is a subtle bug because it doesn't throw a syntax error. Your code just seems to "finish" too early, and the asynchronous operations happen later, detached from your main execution flow. This can lead to race conditions where you try to use data before it has been populated by the loop.
Code That Breaks:
const userIds = [1, 2, 3];
async function fetchUser(id) {
// Simulate a network request
await new Promise(r => setTimeout(r, 50 * id));
const user = { id, name: `User ${id}` };
console.log(`Fetched ${user.name}`);
return user;
}
async function fetchAllUsersWithForEach() {
const users = [];
console.log('Starting fetch with forEach...');
// ❌ MISTAKE: forEach does not wait for the async callbacks.
userIds.forEach(async (id) => {
const user = await fetchUser(id);
users.push(user);
});
// This line executes immediately, BEFORE any users have been fetched.
console.log(`forEach finished. Users array length: ${users.length}`); // Will log 0!
// The "Fetched User X" logs will appear later, out of order.
}
fetchAllUsersWithForEach();
Why This Happens:
Array.prototype.forEach is not "promise-aware." It is a
synchronous method that executes a callback for each element. The
async keyword on the callback just makes that
individual callback return a promise, but
forEach does nothing with that returned promise. It
simply discards it and moves to the next iteration.
The Fix:
const userIds = [1, 2, 3];
async function fetchUser(id) {
await new Promise(r => setTimeout(r, 50 * id));
const user = { id, name: `User ${id}` };
console.log(`Fetched ${user.name}`);
return user;
}
async function fetchAllUsersWithForOf() {
const users = [];
console.log('Starting fetch with for...of...');
// âś… FIX: Use a for...of loop, which respects await.
for (const id of userIds) {
const user = await fetchUser(id); // The loop pauses at each iteration.
users.push(user);
}
console.log(`for...of finished. Users array length: ${users.length}`); // Will log 3.
}
// Alternative fix for parallel execution
async function fetchAllUsersWithPromiseAll() {
console.log('Starting fetch with Promise.all...');
const userPromises = userIds.map(id => fetchUser(id)); // .map is synchronous and fine here
const users = await Promise.all(userPromises);
console.log(`Promise.all finished. Users array length: ${users.length}`); // Will log 3.
}
fetchAllUsersWithForOf(); // For sequential
// fetchAllUsersWithPromiseAll(); // For parallel
Prevention Strategy: As a rule of thumb, avoid using
async callbacks with forEach. If you need to
perform asynchronous operations sequentially for each item in an
array, use a for...of loop. If you want to perform them
all in parallel for maximum performance, use
Array.prototype.map to create an array of promises and
then await the result of Promise.all().
🛠️ Progressive Exercise Set
Exercise 1: Warm-Up (Beginner)
-
Task: You have a function
getQuotethat uses.then(). Convert this function to use theasync/awaitsyntax. - Starter Code:
function getQuoteThen() {
console.log("Fetching quote with .then()...");
fetch('https://api.quotable.io/random')
.then(response => response.json())
.then(data => {
console.log(`Quote: "${data.content}" - ${data.author}`);
})
.catch(error => {
console.error("Could not fetch quote:", error);
});
}
// TODO: Create a new function `getQuoteAsync` that does the same thing using async/await.
// getQuoteAsync();
-
Expected Behavior: When you call
getQuoteAsync(), it should log "Fetching quote..." and then the quote and author to the console, similar to the original function. - Hints:
-
You'll need to declare your new function with the
asynckeyword. -
Use
awaittwice: once for thefetchand once for the.json()call. -
Wrap your
awaitcalls in atry...catchblock to handle potential errors. -
Solution Approach: Create a function
getQuoteAsync. Inside, create atryblock. In thetryblock,awaitthe result of thefetchcall and store it in aresponsevariable. Then,awaitthe result ofresponse.json()and store it in adatavariable. Log the contents ofdata. Add acatchblock to log any errors.
Exercise 2: Guided Application (Beginner-Intermediate)
-
Task: Write an
asyncfunctiongetWeatherthat takes a city name. It should first fetch the geographic coordinates for that city from one API, and then use those coordinates to fetch the weather from a second API. - Starter Code:
// API docs:
// Geocoding: https://geocoding-api.open-meteo.com/v1/search?name=CITY_NAME
// Weather: https://api.open-meteo.com/v1/forecast?latitude=LAT&longitude=LON¤t_weather=true
async function getWeather(city) {
console.log(`Getting weather for ${city}...`);
try {
// Step 1: Fetch coordinates for the city
// Construct the geocoding URL
// Await the fetch call
// Await the response.json()
// Extract latitude and longitude from the first result
// Step 2: Use coordinates to fetch the weather
// Construct the weather URL using the lat and lon
// Await the fetch call
// Await the response.json()
// Step 3: Log the result
// console.log(`The current temperature in ${city} is ...`);
} catch (error) {
console.error(`Failed to get weather for ${city}:`, error);
}
}
getWeather('Berlin');
-
Expected Behavior: The console should log the
fetching process and finally output the current temperature for the
specified city. For "Berlin", it might look like:
The current temperature in Berlin is 15.3°C. - Hints:
-
The geocoding API returns an object with a
resultsarray. You'll want thelatitudeandlongitudefromresults[0]. -
The weather API returns an object with a
current_weatherproperty, which containstemperature. -
Remember to handle the case where a city might not be found. The
geocoding API might return an empty
resultsarray. -
Solution Approach: Inside the
tryblock, construct the geocoding URL andawaitthe fetch. Parse the JSON. Check ifdata.resultsexists and has items. If so, extract latitude and longitude. Use these to build the weather API URL.awaitthe fetch for weather, parse the JSON, and log thecurrent_weather.temperature. If the city isn't found, throw an error to be caught.
Exercise 3: Independent Challenge (Intermediate)
-
Task: You are given an array of Pokémon names.
Write a function
fetchPokemonAbilitiesthat fetches the data for each Pokémon in parallel and logs the name and number of abilities for each one. - Starter Code:
const pokemonNames = ["pikachu", "bulbasaur", "charizard"];
// API Endpoint: https://pokeapi.co/api/v2/pokemon/NAME
async function fetchPokemonAbilities(names) {
// Your code here
}
fetchPokemonAbilities(pokemonNames);
-
Expected Behavior: The function should log the
results to the console. The order might vary because of parallel
execution.
pikachu has 3 abilities. charizard has 2 abilities. bulbasaur has 2 abilities. - Hints:
-
Use
Array.prototype.mapto transform the array of names into an array of Promises. -
The API call for a single Pokémon is
fetch('https://pokeapi.co/api/v2/pokemon/pikachu'). -
Use
Promise.allto wait for all the fetch requests to complete. -
The number of abilities can be found in the
abilitiesarray in the returned JSON data. -
Solution Approach: Create an
asyncfunctionfetchPokemonAbilities. Inside, usenames.mapto create an array of promises. The map callback should be anasyncfunction that takes a name, fetches the data, and returns a promise that resolves to the parsed JSON.awaitPromise.all. Then, loop over the results (which will be an array of Pokémon data objects) and log the name andabilities.length.
Exercise 4: Real-World Scenario (Intermediate-Advanced)
-
Task: Create a function
findFastestMirrorthat takes an array of download server URLs. It should try to fetch a small file (/latency-test.txt) from each server simultaneously and return the URL of the server that responds the fastest. - Starter Code:
const mirrors = [
'https://cdn.jsdelivr.net/gh/jquery/jquery@3.6.0/', // Fast
'http://slowwly.robertomurray.co.uk/delay/2000/url/https://google.com', // Slow
'https://cdn.jsdelivr.net/gh/angular/angular@1.8.2/' // Fast
];
const testFile = 'LICENSE.txt'; // A common small file name
// This function should return the URL of the fastest responding server.
async function findFastestMirror(serverUrls) {
// Your code here
}
findFastastMirror(mirrors).then(fastest => {
console.log(`Fastest mirror is: ${fastest}`);
});
-
Expected Behavior: The console should log the URL
of the server that responded first. Given the list, this will likely
be one of the
cdn.jsdelivr.netURLs. - Hints:
-
Promise.race()is the perfect tool for this job. It takes an array of promises and resolves or rejects as soon as the first promise in the array resolves or rejects. - You'll need to map the server URLs to an array of fetch promises.
-
A challenge is that
Promise.racewill give you the response object, not the URL. How can you know which URL won? You might need to make your promises resolve to an object containing both the response and the original URL. -
Solution Approach: Inside
findFastestMirror, map overserverUrls. For each URL, create a promise that fetches${url}${testFile}. The clever part is to make this promise resolve to an object like{ url: url, response: response }. Pass this array of promises toPromise.race.awaitthe result. The resolved value will be the object from the fastest promise, from which you can extract theurl.
Exercise 5: Mastery Challenge (Advanced)
-
Task: Write an
asyncfunctionprocessUserListthat fetches a list of users, and then for each user, fetches their first post. The API is unreliable and sometimes fails. Your function must be resilient: if fetching a user's post fails, it should be skipped, but the process should continue for other users. The function should return an array of objects, each containing the user's name and the title of their first post. - Starter Code:
// User list: https://jsonplaceholder.typicode.com/users
// Posts for a user: https://jsonplaceholder.typicode.com/posts?userId=USER_ID
async function processUserList() {
// 1. Fetch the list of all users. Handle potential errors.
// 2. Map over the users to create an array of promises, where each promise
// fetches the posts for a single user.
// 3. IMPORTANT: Use Promise.allSettled() to wait for all post fetches.
// This ensures that even if some promises reject, you get the results
// of the ones that succeeded.
// 4. Filter through the results from allSettled, keeping only the fulfilled ones.
// 5. Format the final output.
return []; // Return the formatted array
}
processUserList().then(results => {
console.log("Processed User Posts:", results);
});
-
Expected Behavior: The console should log an array
of objects. Each object should have a
userNameand afirstPostTitleproperty. The array should contain results for all users whose posts were fetched successfully. - Hints:
-
Promise.allSettled()is key. It returns an array of result objects, each with astatus('fulfilled' or 'rejected') and avalueorreason. -
After getting the user list, you can use
mapto kick off all the post fetches in parallel. -
You will need to filter the results of
Promise.allSettledto ignore the rejected promises. - Remember to also extract the original user's name to include in the final output. You might need to pass it along through the promise chain.
-
Solution Approach: First,
awaitthe fetch for the list of users inside atry/catch. Then, map theusersarray to an array of promises. The mapping function should beasyncand for each user, it shouldawaittheir posts, and return a new object{ userName: user.name, firstPostTitle: posts[0].title }. Wrap this entire mapping process inPromise.allSettled.awaitthe result ofallSettled. Finally, filter this result array for entries wherestatus === 'fulfilled'and map that filtered array to just return thevalue.
🏠Production Best Practices
When to Use This Pattern
Scenario 1: Sequential API Calls (Data Dependency)
When you need to perform a series of asynchronous operations where
each step depends on the result of the previous one. This is the
canonical use case for sequential await.
// Get user preferences, then use those preferences to fetch a personalized content feed.
async function loadUserFeed(userId) {
const preferences = await api.getUserPreferences(userId);
const feed = await api.getFeed({ category: preferences.favoriteCategory });
renderFeed(feed);
}
This pattern is appropriate because you cannot know which feed to fetch until you have the user's preferences. The code is clean, linear, and easy to follow.
Scenario 2: Simplifying Complex Conditional Logic
When your asynchronous logic involves if/else statements,
loops, or other control structures. async/await allows
you to integrate async calls seamlessly within these structures.
// Check a cache first. If it's a miss, fetch from the database and update the cache.
async function getArticle(articleId) {
let article = cache.get(articleId);
if (!article) {
console.log("Cache miss. Fetching from database...");
article = await db.articles.find(articleId);
await cache.set(articleId, article);
}
return article;
}
Doing this with .then() chains would be significantly
more complex and harder to read, likely involving nested promises or
awkward promise chaining.
Scenario 3: Parallel Operations with
Promise.all
When you need to perform multiple independent asynchronous operations and wait for all of them to complete before proceeding. This is a crucial performance optimization.
// Load all data needed for a web page in parallel on initial load.
async function initialPageLoad() {
const [headerData, mainContent, sidebarWidgets] = await Promise.all([
api.fetchHeader(),
api.fetchMainContent(),
api.fetchSidebar()
]);
renderPage(headerData, mainContent, sidebarWidgets);
}
This ensures the user waits for the duration of the longest single request, not the sum of all requests, dramatically improving perceived performance.
When NOT to Use This Pattern
Avoid When: You need to "fire-and-forget" an
operation. Use Instead: Don't await the
promise.
If you need to kick off an asynchronous task but your code doesn't
need to wait for its result, then awaiting it is
unnecessary and will slow down your function. This is common for
logging, analytics, or background sync tasks.
// GOOD: We don't need to wait for the analytics event to be sent
// before showing the user their content.
async function user_logs_in(userId) {
// Fire-and-forget the analytics event. Handle potential errors separately if needed.
analytics.track('user_login', { userId }).catch(e => console.error("Analytics failed", e));
// Immediately fetch and return the user's data without waiting.
const userData = await api.getUser(userId);
return userData;
}
Avoid When: The operation is purely CPU-bound, not I/O-bound. Use Instead: Web Workers or child processes.
async/await is designed for I/O operations (network,
disk, database) where the program is waiting for an external resource.
It does not make single-threaded, CPU-intensive tasks
(like complex calculations, image processing, or data compression) run
in parallel. A long-running synchronous calculation inside an
async function will still block the main thread.
// This is CPU-bound and will freeze the UI even inside an async function.
// A Web Worker would be the correct tool here.
async function calculateComplexReport(data) {
// BAD: This blocks the event loop for a long time.
// const result = heavySynchronousCalculation(data); // This is the problem
// BETTER (conceptual): Offload to a worker
const worker = new Worker('report-worker.js');
worker.postMessage(data);
return new Promise(resolve => {
worker.onmessage = e => resolve(e.data);
});
}
Performance & Trade-offs
Time Complexity: The time complexity of a series of
await calls is the sum of the time for each awaited
operation, O(T1 + T2 + ... + Tn). For parallel operations with
Promise.all, the time complexity is the time of the
longest-running operation, O(max(T1, T2, ..., Tn)). This is why
parallelizing is so important for performance. For example, three
1-second API calls take ~3 seconds sequentially but only ~1 second in
parallel.
Space Complexity: Each async function
call creates a new promise and a function context that must be stored
in memory until the promise is settled. If you have deeply nested
chains of async calls, this can increase memory usage
compared to a purely callback-based approach, though in most practical
applications this overhead is negligible and well worth the
readability trade-off.
Real-World Impact: The biggest performance impact
comes from avoiding unintentional waterfalls. A team that defaults to
Promise.all for independent tasks will build a much
faster application than a team that uses sequential
await for everything. The overhead of the
async/await syntax itself is minimal in modern JavaScript
engines.
Debugging Considerations: async/await is
a massive improvement for debugging. When an error is thrown, the call
stack shown in developer tools often traces through the
await calls, making it look like a synchronous stack
trace. This makes it far easier to pinpoint where an error originated
compared to the fragmented, often unhelpful call stacks from
.then() chains or callbacks.
Team Collaboration Benefits
Readability: async/await makes
asynchronous code read like a simple, top-down story. A developer can
look at a function and immediately understand the sequence of events
without having to mentally parse .then() chains or
callback nesting. This dramatically lowers the cognitive load and
makes code reviews more effective, as reviewers can focus on the
business logic rather than deciphering complex asynchronous flow
control.
Maintainability: Code that is easier to read is
easier to maintain. When a new requirement comes in—for example,
adding another asynchronous step in a sequence—it's as simple as
adding a new await line. Refactoring is also simpler. The
familiar try...catch block provides a standardized and
robust way to handle errors for a whole block of operations, making it
much easier to add or modify error handling logic consistently across
the codebase.
Onboarding: For new developers, especially those
coming from synchronous languages like Python or C#,
async/await provides a much gentler learning curve. The
syntax aligns with their existing mental models of how code should
execute. This means they can become productive contributors to the
asynchronous parts of a JavaScript codebase much more quickly, without
the steep initial hurdle of mastering Promise-chaining or callback
patterns.
🎓 Learning Path Guidance
If this feels comfortable:
-
Next Challenge: Implement a "rate limiter" for an
API client. Create an
asyncfunction that takes a list of tasks (e.g., functions that return promises) and executes them with a specified concurrency, for example, no more than 3 requests at a time. -
Explore Deeper: Investigate JavaScript Generators
and the
yieldkeyword.async/awaitis largely based on the concepts pioneered by generators, and understanding them will give you a much deeper appreciation for howawaitactually pauses and resumes a function. -
Connect to: How frameworks like Express.js
(Node.js) handle
asyncmiddleware. An Express route handler can be anasyncfunction, and any error thrown inside will be caught by the framework's error handling middleware, which is a powerful pattern.
If this feels difficult:
-
Review First: Revisit the core concepts of Promises
(
new Promise,resolve,reject,.then(),.catch()).async/awaitis just a different way to work with Promises, so a solid foundation there is essential. -
Simplify: Forget about
Promise.allfor a moment and focus only on a simple sequence of twoawaitcalls. UsesetTimeoutwithin aPromiseto simulate network delays so you can see the pausing effect clearly. -
Focus Practice: Write five different simple
asyncfunctions. Convert a function that uses.then()toasync/await. Convert one back fromasync/awaitto.then(). This back-and-forth practice will solidify your understanding. - Alternative Resource: Search for visual explainers or videos on the JavaScript "Event Loop." Understanding how the event loop, callback queue, and main thread interact is fundamental to grasping why pausing is possible without blocking the entire program.
---
Day 18-21: Advanced Async Patterns
🎯 Learning Objectives
-
By the end of this day, you will be able to assign the resolved
value of a Promise to a new or existing variable using
await. -
By the end of this day, you will be able to implement conditional
asynchronous logic using
awaitwithinif/elseblocks and ternary operators. - By the end of this day, you will be able to create reusable higher-order asynchronous functions, such as an automatic retry utility.
-
By the end of this day, you will be able to distinguish between and
apply different
Promisestatic methods likerace,allSettled, andanywithinasyncfunctions.
📚 Concept Introduction: Why This Matters
Paragraph 1 - The Problem: While basic sequential
await calls are powerful, real-world applications demand
far more sophisticated asynchronous logic. Developers constantly face
challenges like: "How do I fetch data from source A, but only if a
certain condition is met?" or "How can I fetch from multiple redundant
sources and take whichever one responds first?" or "How do I make a
critical API call more resilient to temporary network failures without
littering my code with repetitive error handling?" Trying to solve
these problems with simple Promise chains or basic
await sequences can lead to convoluted, hard-to-read code
that is brittle and difficult to test.
Paragraph 2 - The Solution: The advanced patterns
covered today leverage the simple act of assigning the result of an
await call to a variable (const result = await ...
or let result; result = await ...). This seemingly small
step is the key that unlocks the full power of
async/await. By capturing the resolved value in a
variable, we can then use it in all the familiar synchronous control
flow structures JavaScript offers. We can use it in an
if statement to conditionally run another async
operation. We can reassign it inside a while loop to
implement retries. We can use a ternary operator to concisely choose
between two different async sources. This allows us to compose and
orchestrate complex asynchronous workflows with the same clarity and
expressiveness as synchronous logic.
Paragraph 3 - Production Impact: Mastering these
assignment and control flow patterns is what separates a junior
developer from a senior one when it comes to asynchronous JavaScript.
Professional teams build robust, production-grade services by creating
reusable asynchronous utilities, such as a generic
fetchWithRetry function, which drastically reduces
boilerplate and improves application reliability. They use conditional
fetching to build efficient, dynamic user interfaces that only request
the data they need. By using tools like Promise.race for
timeouts or Promise.allSettled for handling partial
failures, they build resilient systems that can gracefully degrade
instead of crashing entirely. These patterns lead to code that is not
only more powerful and efficient but also more declarative, testable,
and maintainable in the long run.
🔍 Deep Dive: const IDENTIFIER = await EXPRESSION and
IDENTIFIER = await EXPRESSION
Pattern Syntax & Anatomy
// This pattern declares a new variable and assigns the resolved Promise value to it.
async function fetchAndDeclare() {
// `const` is used when the variable will not be reassigned.
const userData = await fetch('/api/user/1');
// ↑ ↑ ↑ ↑
// Keyword Identifier = await keyword + Promise-returning expression
return await userData.json();
}
// This pattern assigns a resolved Promise value to a pre-existing variable.
async function fetchConditionally(useCache, id) {
let data; // `let` must be used as the variable will be reassigned.
// ↑ ↑
// Keyword Identifier
if (useCache) {
data = getFromCache(id);
} else {
// Re-assignment of the `data` variable.
data = await fetchFromDatabase(id);
// ↑ ↑ ↑
// Identifier = await keyword + Promise-returning expression
}
return data;
}
How It Actually Works: Execution Trace
Let's trace the execution of the
fetchConditionally function, assuming
useCache is false:
Step 1: The fetchConditionally(false, 123) function is
called. It immediately returns a pending Promise.
Step 2: Inside the function, a variable data is declared
with let and its initial value is undefined.
Step 3: The if (useCache) condition is checked. Since
it's false, the else block is entered.
Step 4: The line data = await fetchFromDatabase(id) is
encountered. The fetchFromDatabase(123) function is
called, which presumably returns a Promise.
Step 5: The await keyword sees the Promise. It pauses the
execution of fetchConditionally and gives control back to
the event loop, waiting for the database operation to complete.
Step 6: Sometime later, the database operation finishes and the
Promise it returned resolves with the fetched data (e.g.,
{ name: 'Product' }).
Step 7: The fetchConditionally function resumes
execution. The resolved value { name: 'Product' } is
assigned to the data variable.
Step 8: The function continues to the next line,
return data;. The value of data is returned,
which resolves the original Promise from Step 1 with the fetched data.
Example Set (REQUIRED: 6 Complete Examples)
Example 1: Foundation - Simplest Possible Usage
// A simple Promise that resolves with an object after a delay.
function getConfiguration() {
return new Promise(resolve => {
setTimeout(() => {
resolve({ theme: 'dark', version: '1.2.0' });
}, 500);
});
}
// The async function that uses the assignment pattern.
async function loadApp() {
console.log('Loading configuration...');
// Await the promise and assign its resolved value to `config`.
const config = await getConfiguration();
// Now `config` can be used like any regular object.
console.log(`Applying ${config.theme} theme.`);
console.log(`App version: ${config.version}`);
}
loadApp();
// Expected output:
// Loading configuration...
// (after 500ms)
// Applying dark theme.
// App version: 1.2.0
This foundational example clearly shows the core pattern:
await pauses execution, and its resolved value is stored
in the config constant, making it available for
subsequent synchronous operations.
Example 2: Practical Application
// Real-world scenario: Conditional data fetching based on user permissions.
async function getUserData(userId) { /* returns { isAdmin: boolean } */
return { id: userId, isAdmin: userId === 1 };
}
async function getAdminDashboardData() { /* returns admin data */
return { report: 'Sales figures...' };
}
async function getStandardUserData() { /* returns regular data */
return { profile: 'User profile...' };
}
async function loadPage(userId) {
console.log(`Loading page for user ${userId}`);
const user = await getUserData(userId);
let dashboardData; // Declare with `let` because it's conditionally assigned.
if (user.isAdmin) {
console.log('User is an admin. Loading admin dashboard.');
dashboardData = await getAdminDashboardData();
} else {
console.log('User is standard. Loading user profile.');
dashboardData = await getStandardUserData();
}
console.log('Data loaded:', dashboardData);
return dashboardData;
}
loadPage(1); // will load admin data
// loadPage(2); // will load standard data
This demonstrates a common production pattern where an initial
asynchronous call determines which subsequent asynchronous operations
are needed. Using let and conditional assignment makes
the logic clear and readable.
Example 3: Handling Edge Cases
// What happens with an await in a ternary operator?
async function checkServerStatus() {
// Simulate a 50/50 chance of the server being "primary"
const isPrimary = Math.random() > 0.5;
return { primary: isPrimary };
}
async function fetchDataFromPrimary() { return "Data from Primary Server"; }
async function fetchDataFromReplica() { return "Data from Replica Server"; }
async function getReplicatedData() {
console.log('Checking which server is primary...');
const status = await checkServerStatus();
// The entire `await` expression is evaluated before the assignment.
// This is a concise way to handle simple conditional async logic.
const data = status.primary
? await fetchDataFromPrimary()
: await fetchDataFromReplica();
console.log(`Received: "${data}"`);
return data;
}
getReplicatedData();
This example shows how await can be used within
expressions like the ternary operator. It's a concise and powerful
pattern for simple conditional logic, but it's important to understand
that only one of the awaited functions (fetchDataFromPrimary
or fetchDataFromReplica) will actually be executed.
Example 4: Pattern Combination
// Combining assignment with a for...of loop for sequential processing.
const orderIds = [101, 102, 103, 104];
// Simulates checking inventory, which takes time.
async function checkInventory(orderId) {
await new Promise(r => setTimeout(r, 200));
// In a real app, this might return true/false if stock is available.
const hasStock = Math.random() > 0.2;
console.log(`Order ${orderId} has stock: ${hasStock}`);
return hasStock;
}
// Simulates processing a payment.
async function processPayment(orderId) {
await new Promise(r => setTimeout(r, 300));
console.log(`Payment processed for order ${orderId}.`);
return { success: true };
}
async function processOrdersSequentially() {
console.log("Starting order processing...");
const processedOrders = [];
const failedOrders = [];
// The for...of loop pauses on each `await`, ensuring orders are processed one by one.
for (const id of orderIds) {
const hasStock = await checkInventory(id);
if (hasStock) {
const paymentResult = await processPayment(id);
if(paymentResult.success) processedOrders.push(id);
} else {
failedOrders.push(id);
}
}
console.log("Processing complete.");
console.log("Successfully processed:", processedOrders);
console.log("Failed due to lack of stock:", failedOrders);
}
processOrdersSequentially();
This demonstrates when a sequential loop is the
correct choice. Because processing a payment should only
happen after successfully checking inventory, we must use a
for...of loop to handle each order one at a time. The
assignment of await results to variables within the loop
is crucial for the logic.
Example 5: Advanced/Realistic Usage
// Production-level implementation: A higher-order "with-timeout" function.
// This utility takes an async operation and a timeout duration.
// It races the operation against a timer.
async function withTimeout(promise, timeoutMs) {
let timeoutId = null;
// Create a promise that rejects after a delay.
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`Operation timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
try {
// Race the input promise against our timeout promise.
const result = await Promise.race([promise, timeoutPromise]);
return result;
} finally {
// IMPORTANT: Clear the timeout to prevent memory leaks if the original promise finishes first.
clearTimeout(timeoutId);
}
}
// A slow function to test the timeout utility.
async function slowApiCall() {
console.log('Slow API call started...');
await new Promise(resolve => setTimeout(resolve, 5000));
return 'Here is your data!';
}
// Now let's use our utility.
async function main() {
try {
console.log('Calling API with a 2-second timeout...');
const data = await withTimeout(slowApiCall(), 2000);
console.log('Success:', data);
} catch (error) {
console.error('Caught error:', error.message);
}
}
main();
This professional-grade utility demonstrates a powerful composition
pattern. withTimeout is a reusable async function that
enhances other async operations. The assignment
const result = await Promise.race(...) is the core of its
logic, capturing whichever promise settles first.
Example 6: Anti-Pattern vs. Correct Pattern
// ❌ ANTI-PATTERN - Shadowing a variable inside a conditional block.
async function fetchConfig(env) {
let config = { default: true }; // Outer scope `config`
if (env === 'production') {
// Using `const` or `let` here creates a NEW, block-scoped variable.
// The outer `config` is never modified.
const config = await fetch('/api/prod-config');
console.log("Fetched prod config:", config);
} else if (env === 'staging') {
// This also creates a new `config` variable.
const config = await fetch('/api/staging-config');
console.log("Fetched staging config:", config);
}
// This will always return the default config object.
return config;
}
// âś… CORRECT APPROACH - Assigning to the outer-scoped variable.
async function fetchConfigCorrect(env) {
// Declare with `let` so it can be reassigned.
let config = { default: true };
if (env === 'production') {
// No `const` or `let` here. We are ASSIGNING to the existing variable.
config = await fetch('/api/prod-config');
console.log("Fetched prod config:", config);
} else if (env === 'staging') {
// Same here - assignment, not declaration.
config = await fetch('/api/staging-config');
console.log("Fetched staging config:", config);
}
// This now returns the fetched config correctly.
return config;
}
The anti-pattern makes a classic JavaScript scope mistake. By
re-declaring config with const inside the
if blocks, it creates new variables that only exist
within those blocks ("shadowing" the outer variable). The correct
approach properly declares the variable with let in the
outer scope and then reassigns its value within the
conditional blocks, achieving the intended behavior.
⚠️ Common Pitfalls & Solutions
Pitfall #1: Misunderstanding Top-Level await
What Goes Wrong: Developers learn they must use
await inside an async function and often try
to use it at the very top level of their script file, outside of any
function, leading to a
SyntaxError: await is only valid in async functions and the top
level bodies of modules. This error message can be confusing. "Top level bodies of modules"
is the key part.
This means await can be used at the top level, but only
if the JavaScript file is treated as an ES Module. This is typically
done by using <script type="module"> in HTML, or by
using .mjs file extensions or
{"type": "module"} in package.json in
Node.js. If the script is run as a classic script, top-level
await is forbidden. This leads to frustration when code
that works in one environment (e.g., a modern bundler setup) breaks in
another (e.g., a simple HTML file).
Code That Breaks:
// In a file named 'script.js' included via <script src="script.js"></script>
// This will throw a SyntaxError.
console.log('Fetching initial data...');
// ❌ MISTAKE: `await` used at the top level of a non-module script.
const initialData = await fetch('https://api.example.com/data');
console.log('Data loaded:', initialData);
function main() {
// ...app logic...
}
main();
Why This Happens: Classic scripts execute
synchronously from top to bottom. Allowing await at the
top level would block the parsing and execution of the rest of the
file and potentially other scripts on the page, which was deemed
undesirable. ES Modules have a different, more sophisticated loading
and execution mechanism (defer is the default) that makes
top-level await feasible and safe, as it can pause the
module's execution without blocking other resources.
The Fix:
// âś… FIX 1: Wrap the logic in an async IIFE (Immediately Invoked Function Expression).
// This is the classic, universally-compatible solution.
(async function() {
console.log('Fetching initial data...');
try {
const response = await fetch('https://api.example.com/data');
const initialData = await response.json();
console.log('Data loaded:', initialData);
function main() {
// ...app logic...
}
main();
} catch(e) {
console.error("Failed to initialize app", e);
}
})();
// âś… FIX 2: Treat the script as a module.
// In HTML: <script type="module" src="script.js"></script>
// Now the original "Code That Breaks" would work correctly in this context.
Prevention Strategy: For broadest compatibility,
especially if you're not sure about the execution environment, the
safest pattern is to wrap your top-level asynchronous startup logic in
an async IIFE. If you are working in a modern environment
that explicitly uses ES Modules (like Vite, Next.js, or a recent
Node.js project), then you can safely use top-level
await, but be aware of its context.
Pitfall #2: Redundant await on Return
What Goes Wrong: A common stylistic and minor
performance issue is redundantly awaiting a promise when it's the very
last thing a function does. For example,
const result = await someAsyncFunc(); return result;.
While this code works perfectly fine, it's slightly less efficient and
more verbose than necessary.
An async function will automatically wrap any returned
value in a resolved Promise. If you return a Promise (which is what
someAsyncFunc() gives you), the
async function will simply adopt the state of that
returned promise. Awaiting it first means the current function pauses
and waits for the inner promise to resolve, only to then create a new
resolved promise with that same value. It's an unnecessary
intermediate step.
Code That Breaks:
async function getPost(id) {
// This returns a promise
return fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
}
// ❌ SLIGHTLY INEFFICIENT: Unnecessary await
async function getAndProcessPost(id) {
try {
const response = await getPost(id);
const post = await response.json();
// There is no more logic after this, so awaiting is redundant.
return await processPost(post); // `processPost` is also async
} catch (error) {
console.error("Failed:", error);
// Returning a value from a catch block will result in a resolved promise.
return { error: true };
}
}
async function processPost(post) { /* does some async work */ return post; }
Why This Happens: This is usually just a habit from
thinking "I have a promise, I must await it." The
async function's internal machinery is smart enough to
handle a returned Promise transparently. By awaiting, you're adding an
extra "tick" to the event loop before the promise chain can continue.
The Fix:
async function getPost(id) {
return fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
}
// âś… MORE EFFICIENT: Directly return the promise
async function getAndProcessPostFixed(id) {
try {
const response = await getPost(id);
const post = await response.json();
// Simply return the promise. The `async` function will chain to it.
return processPost(post);
} catch (error) {
console.error("Failed:", error);
return { error: true };
}
}
async function processPost(post) { /* does some async work */ return post; }
Prevention Strategy: If the very last statement in
your async function's try block is
return await somePromise(), you can almost always safely
remove the await. This makes the code slightly more
performant and concise. However, be careful: if you have a
finally block, you might want to keep the
await to ensure the finally block executes
after the awaited operation completes, not just after it's
been initiated.
Pitfall #3: Mixing await and
.then() Error Handling
What Goes Wrong: When developers mix
async/await with .then() or
.catch() chains, error handling can become very
confusing. If you await a promise that also has a
.catch() block attached to it, the
.catch() block will "handle" the error. This means the
promise will no longer be in a rejected state; instead, it will
resolve with whatever value the .catch() block returns
(undefined by default).
As a result, the try...catch block surrounding your
await will never be triggered, because from its
perspective, the promise never rejected. The error is silently
swallowed by the lower-level .catch(), and your function
continues executing with potentially undefined data,
leading to downstream errors that are much harder to debug.
Code That Breaks:
function willReject() {
return Promise.reject(new Error("Something went wrong!"));
}
async function mixedErrorHandling() {
try {
console.log("Calling function...");
// ❌ MISTAKE: The .catch() handles the error, so the outer try...catch never sees it.
const result = await willReject().catch(err => {
console.log("Inner .catch() was triggered:", err.message);
// It returns undefined, so `result` will be undefined.
});
// This code runs because the error was "handled".
console.log("This line should not be reached!");
console.log("Result is:", result); // Logs "Result is: undefined"
} catch (e) {
// This block is NEVER executed.
console.log("Outer try...catch was triggered:", e.message);
}
}
mixedErrorHandling();
Why This Happens: A .catch() block
returns a new Promise. If the original promise rejects, the
.catch() callback runs. The new Promise will then resolve
with the return value of that callback. If the callback doesn't return
anything, it resolves with undefined. The
await keyword then receives this resolved value, and the
try...catch is none the wiser that an error ever
occurred.
The Fix:
function willReject() {
return Promise.reject(new Error("Something went wrong!"));
}
async function correctErrorHandling() {
try {
console.log("Calling function...");
// âś… FIX: Do not attach .catch(). Let the `await` handle the rejection.
const result = await willReject();
console.log("This line will not be reached.");
} catch (e) {
// The rejection is now correctly caught here.
console.log("Outer try...catch was triggered:", e.message);
}
}
correctErrorHandling();
Prevention Strategy: Commit to one style of error
handling within an async function. If you are using
async/await, use try...catch exclusively for
handling errors from awaited promises. Avoid attaching
.catch() to a promise that you are also planning to
await, unless you have a very specific reason to
transform a rejection into a resolved value at that specific point.
🛠️ Progressive Exercise Set
Exercise 1: Warm-Up (Beginner)
-
Task: Create an
asyncfunctioncheckStatusthat fetches data from an API. Assign theresponseto a variable. Use theresponse.okproperty in anifstatement to check if the request was successful. If it was, log thestatus, otherwise log an error message with thestatus. - Starter Code:
// API to use: https://httpstat.us/200 (for success) or https://httpstat.us/404 (for failure)
async function checkStatus(url) {
// Use await to get the response and assign it to a variable
// Use an if/else block to check response.ok
// Log the appropriate message
}
checkStatus('https://httpstat.us/200');
checkStatus('https://httpstat.us/404');
- Expected Behavior:
-
For the 200 URL:
Request successful with status: 200 - For the 404 URL:
Request failed with status: 404 - Hints:
-
Remember that
fetch()itself only rejects on network errors, not on HTTP error statuses like 404. -
The
responseobject fromfetchhas a booleanokproperty and a numericstatusproperty. -
Solution Approach: Inside
checkStatus, declare aconst responseand assignawait fetch(url)to it. Then, write anif (response.ok)block. In theifblock,console.loga success message usingresponse.status. In theelseblock,console.loga failure message.
Exercise 2: Guided Application (Beginner-Intermediate)
-
Task: Create a function
getCachedDatathat takes akeyand anasyncfunctionfetcher. It should maintain a simple in-memory cache (aMapobject). If thekeyexists in the cache, return the cached value. If not, call thefetcherfunction, store its result in the cache, and then return the result. - Starter Code:
const cache = new Map();
async function fetcher(key) {
console.log(`-- Executing fetcher for ${key} --`);
await new Promise(r => setTimeout(r, 500)); // Simulate network delay
return { data: `Data for ${key}` };
}
async function getCachedData(key, dataFetcher) {
// 1. Check if `key` is in the `cache` Map.
// 2. If it is, log "Returning from cache" and return the value.
// 3. If it is NOT, log "Fetching new data", call the dataFetcher,
// and assign the result to a variable.
// 4. Store the new data in the cache using cache.set(key, data).
// 5. Return the new data.
}
async function main() {
const data1 = await getCachedData('user:1', () => fetcher('user:1'));
console.log(data1);
const data2 = await getCachedData('user:1', () => fetcher('user:1')); // Should be cached
console.log(data2);
}
main();
-
Expected Behavior: The console should show that the
fetcher is only executed once. The second call should hit the cache.
Fetching new data -- Executing fetcher for user:1 -- { data: 'Data for user:1' } Returning from cache { data: 'Data for user:1' } - Hints:
-
Use
cache.has(key)to check for existence andcache.get(key)to retrieve data. -
You'll need a variable declared with
letto hold the data, as it might be assigned from the cache or from the fetcher. -
Solution Approach: Check
if (cache.has(key)). If true, returncache.get(key). If false,awaitthedataFetcher, assign the result to anewDatavariable. Then, callcache.set(key, newData)and finallyreturn newData.
Exercise 3: Independent Challenge (Intermediate)
-
Task: Write an
asyncfunctiongetPrimaryUserAndPoststhat fetches a list of users. It should then find the first user whoseaddress.geo.latis less than 0 (i.e., in the southern hemisphere). Once found, it should then fetch all posts for that user and return an object containing the user'snameand theirposts. - Starter Code:
const USERS_URL = 'https://jsonplaceholder.typicode.com/users';
const POSTS_URL_TEMPLATE = 'https://jsonplaceholder.typicode.com/posts?userId=';
async function getPrimaryUserAndPosts() {
// Your code here.
// 1. Fetch all users.
// 2. Find the correct user using Array.find().
// 3. If a user is found, fetch their posts using their ID.
// 4. Return the combined data or null if no such user is found.
}
getPrimaryUserAndPosts().then(result => {
console.log(result);
});
-
Expected Behavior: The function will log an object
containing the name of the first user from the southern hemisphere
and an array of all their posts. e.g.,
{ name: 'Clementine Bauch', posts: [...] }. - Hints:
-
Remember to
awaitthe response fromfetchand thenawaitthe.json()call. -
Array.prototype.find()can be used to locate the user in the array. -
You will need to construct the posts URL using the found user's
id. -
Solution Approach:
awaitthe fetch fromUSERS_URLand parse the JSON, assigning it to ausersvariable. Then useconst primaryUser = users.find(...)with the correct condition. Add anif (primaryUser)check. Inside,awaitthe fetch for posts usingprimaryUser.idand parse the JSON into apostsvariable. Finally, return{ name: primaryUser.name, posts: posts }. Returnnulloutside theifblock.
Exercise 4: Real-World Scenario (Intermediate-Advanced)
-
Task: Create a function
fetchWithAllSettledthat takes an array of URLs. It should fetch all of them in parallel. It must return an object with two properties:successful, an array of the JSON data from the successfully fetched URLs, andfailed, an array of error reasons for the URLs that failed. - Starter Code:
const urls = [
'https://jsonplaceholder.typicode.com/posts/1',
'https://jsonplaceholder.typicode.com/invalid-url', // This will fail
'https://jsonplaceholder.typicode.com/posts/2',
'https://jsonplaceholder.typicode.com/another-bad-url'
];
async function fetchWithAllSettled(urls) {
// 1. Map the URLs to an array of fetch promises.
// 2. Use Promise.allSettled to await all of them.
// 3. Process the results array to separate fulfilled and rejected promises.
return { successful: [], failed: [] };
}
fetchWithAllSettled(urls).then(results => {
console.log("Successful:", results.successful.length);
console.log("Failed:", results.failed.length);
console.log("Failed reasons:", results.failed);
});
-
Expected Behavior: The console should log that 2
requests were successful and 2 failed, along with the reasons for
the failures (e.g.,
TypeError: Failed to fetch). - Hints:
-
Promise.allSettledis the perfect tool. Theawaitwill assign an array of result objects to your variable. -
Each result object has a
statusproperty ('fulfilled'or'rejected') and either avalueor areason. -
You can use a
for...ofloop orreduceto iterate over the results and populate yoursuccessfulandfailedarrays. -
Solution Approach: Inside the function,
mapthe URLs to an array of fetch promises.awaitPromise.allSettledon this array. Initializesuccessful = []andfailed = []. Loop through the results. Ifresult.status === 'fulfilled', pushresult.valuetosuccessful.result.valuewill be the Response object, so you'll need another step. A better approach is to make the initial map do thejson()parsing as well:urls.map(async url => { const res = await fetch(url); if (!res.ok) throw ...; return res.json() }). This way thevalueis the data itself. Ifresult.status === 'rejected', pushresult.reason.messagetofailed.
Exercise 5: Mastery Challenge (Advanced)
-
Task: Implement an
asyncfunctionexecuteInSequencethat takes an array of "task functions" (functions that return a Promise). It should execute them one by one, in order. The result of each task should be passed as the input to the next task. The function should return the result of the final task. - Starter Code:
// Example task functions
const task1 = async (input = 1) => {
console.log('Task 1 running...');
await new Promise(r => setTimeout(r, 300));
return input * 2;
};
const task2 = async (input) => {
console.log('Task 2 running...');
await new Promise(r => setTimeout(r, 500));
return input + 5;
};
const task3 = async (input) => {
console.log('Task 3 running...');
await new Promise(r => setTimeout(r, 200));
return input - 3;
};
const tasks = [task1, task2, task3];
async function executeInSequence(tasks) {
// Your code here
}
executeInSequence(tasks).then(finalResult => {
console.log(`Final result: ${finalResult}`); // Should be (1 * 2) + 5 - 3 = 4
});
-
Expected Behavior: The tasks should log their
messages in order, with delays between them. The final output should
be
Final result: 4. - Hints:
-
You can use a
for...ofloop to iterate through thetasksarray. -
You'll need a variable (e.g.,
currentValue) to hold the result of the previous task. Initialize it for the first task. -
In each iteration,
awaitthe call to the current task function, passingcurrentValueas its argument. Then, updatecurrentValuewith the result. -
Solution Approach: Initialize a
let result;. Use afor...ofloop over thetasksarray. Inside the loop, callresult = await task(result);. Theresultvariable will beundefinedon the first iteration, which is why the starter code fortask1provides a default value. After the loop, return the finalresult.
🏠Production Best Practices
When to Use This Pattern
Scenario 1: Conditional Logic / Feature Flags
When you need to fetch different data or follow a different logical path based on a feature flag or user permission that must itself be fetched asynchronously.
// Fetch a feature flag, then use the result to decide which component data to load.
async function loadComponentData(componentId) {
const flags = await featureFlagClient.getFlags();
if (flags.useNewApi) {
const data = await newApi.fetchData(componentId);
return transformNewData(data);
} else {
const data = await oldApi.fetchData(componentId);
return transformOldData(data);
}
}
This pattern of assigning an awaited result (flags) and
using it in standard control flow is fundamental to building dynamic
applications.
Scenario 2: Building Resilient Services
When you need to handle failures gracefully or query multiple sources.
Using assignment with Promise.race for timeouts or
Promise.allSettled for partial success is a hallmark of
robust code.
// Fetch from a primary DB and a replica, take whichever is faster, but within a 1s timeout.
async function getFastestResult(query) {
const primaryPromise = db.primary.query(query);
const replicaPromise = db.replica.query(query);
try {
const result = await withTimeout(Promise.any([primaryPromise, replicaPromise]), 1000);
return result;
} catch (err) {
// This runs if BOTH fail or they BOTH timeout.
log.error("All data sources failed", err);
throw err;
}
}
This code composes multiple advanced async patterns to create a highly resilient data-fetching strategy.
Scenario 3: Creating Reusable Asynchronous Utilities
When you want to encapsulate complex async logic (like retries, caching, or rate limiting) into a reusable function that other developers can use easily.
// A reusable utility that ensures a user is authenticated before running an action.
async function runAsUser(userId, actionAsyncFn) {
const session = await auth.getSession(userId);
if (!session.isValid) {
throw new Error("Session expired");
}
// `actionAsyncFn` is an async function passed as an argument.
const result = await actionAsyncFn(session.authToken);
return result;
}
// Usage:
// const userPosts = await runAsUser(123, (token) => api.getPosts(token));
This Higher-Order Function pattern is extremely powerful for creating clean, Don't-Repeat-Yourself (DRY) asynchronous code.
When NOT to Use This Pattern
Avoid When: The logic can be expressed as a simpler
Promise chain. Use Instead:
.then() chaining.
For a very simple, linear sequence of transformations without
conditional logic, a standard .then() chain can sometimes
be more concise and expressive, especially in a functional programming
style.
// This is arguably cleaner with .then() for a simple transformation pipeline.
function getAndFormatUsername(userId) {
return api.fetchUser(userId)
.then(user => user.name)
.then(name => name.toUpperCase());
}
// The async/await version is slightly more verbose for this specific case.
async function getAndFormatUsernameAsync(userId) {
const user = await api.fetchUser(userId);
const name = user.name;
return name.toUpperCase();
}
Avoid When: Creating a large number of un-awaited
promises in a loop. Use Instead:
Promise.all or a streaming approach.
While it's possible to start many async operations in a loop without
await and then process them later, it can be dangerous.
If you're not careful, you can easily overwhelm downstream services or
run out of memory.
// BAD: This can easily fire thousands of requests at once.
async function updateAllUsers(userIds) {
// This loop doesn't wait, launching all requests immediately.
userIds.forEach(id => {
api.updateUser(id).catch(e => log.error(e));
});
}
// BETTER: Use Promise.all with a batching mechanism for control.
async function updateAllUsersInBatches(userIds, batchSize = 10) {
for (let i = 0; i < userIds.length; i += batchSize) {
const batch = userIds.slice(i, i + batchSize);
const promises = batch.map(id => api.updateUser(id));
await Promise.all(promises);
console.log(`Processed batch starting at index ${i}`);
}
}
Performance & Trade-offs
Time Complexity: The time complexity is determined by
the control flow. An await inside a for loop
results in sequential execution (sum of times), whereas mapping to an
array of promises and using Promise.allSettled results in
parallel execution (time of the longest task). Understanding this
distinction is critical for performance.
Space Complexity: Using Promise.all or
allSettled on a very large array (e.g., 10,000 items)
will create 10,000 promises and their results in memory at once. For
very large datasets, this can cause high memory pressure. In these
scenarios, a sequential for...of loop or a more advanced
streaming/batching approach might be preferable for its lower,
constant memory usage.
Real-World Impact: Using
Promise.allSettled instead of
Promise.all for non-critical, independent tasks can make
a user interface feel more robust. If one of five components fails to
load its data, the other four can still render successfully, improving
the user experience. Misusing loops can easily lead to API
rate-limiting or server overload.
Debugging Considerations: Complex conditional async
logic can still be tricky to debug. If a variable is assigned from one
of several await calls in different
if/else blocks, it might not be immediately obvious which
path was taken. Using descriptive variable names and judicious logging
can help trace the flow of execution through these complex paths.
Team Collaboration Benefits
Readability: These advanced patterns, when used
correctly, make the intent of the code much clearer. A
for...of loop with an await inside clearly
signals "I need to do these things in order." Using
Promise.allSettled clearly signals "I want to try all
these things and I don't mind if some fail." This clarity is
invaluable for team collaboration.
Maintainability: Creating reusable async utilities
like withTimeout or fetchWithRetry is a huge
boon for maintainability. It centralizes complex logic in one
well-tested place. When a bug needs to be fixed or the logic needs to
be updated (e.g., changing the retry strategy), it only needs to be
changed in one file, rather than in dozens of places across the
codebase.
Onboarding: When a project has established,
well-named async utility functions, new developers can be more
productive faster. Instead of having to write complex retry logic from
scratch, they can be told, "For all external API calls, wrap them in
our fetchWithRetry utility." This provides a safe,
consistent framework for them to work within, reducing the chance of
them introducing subtle bugs.
🎓 Learning Path Guidance
If this feels comfortable:
-
Next Challenge: Create a more advanced
withRetryfunction. It should accept anoptionsobject that allows specifying the number ofretries, an initialdelay, and abackoffStrategyfunction (e.g.,(attempt) => delay * attemptfor linear backoff, or(attempt) => delay * Math.pow(2, attempt)for exponential backoff). -
Explore Deeper: Look into asynchronous iterators
and the
for await...ofloop. This is a powerful pattern for working with data that arrives in chunks, such as reading a large file from disk or consuming a streaming API, without loading the entire dataset into memory. -
Connect to: Modern frontend frameworks like React
with Suspense or SvelteKit with its
loadfunctions. These frameworks have built-in mechanisms that deeply integrate withasync/awaitto handle data fetching, loading states, and error states in a declarative way.
If this feels difficult:
-
Review First: Go back to synchronous JavaScript
control flow. Make sure you are 100% comfortable with
letvs.const, variable scope,if/else,for...of, and ternary operators. The complexity here is not inawaititself, but in how it interacts with these fundamental concepts. -
Simplify: Create a "fake" async function using
new Promise(res => setTimeout(() => res('value'), 500)). Use this fake function exclusively to practice conditional logic. For example: write anasyncfunction that calls it; if the result is'value', call it a second time. This removes the complexity of real network requests. -
Focus Practice: Practice the
cacheexercise (Exercise 2) several times. This pattern of "check for value, if missing,awaitits creation, then return" is one of the most common and important advanced patterns. -
Alternative Resource: Read the MDN documentation
for
Promise.all,Promise.allSettled,Promise.race, andPromise.any. Create a small "cheatsheet" for yourself explaining what each one does, what it returns on success, and what it returns on failure.
---
Week 3 Integration & Summary
Patterns Mastered This Week
| Pattern | Syntax | Primary Use Case | Key Benefit |
|---|---|---|---|
| Sequential Await |
const r = await p();
|
For operations that depend on the result of a previous operation. | Guarantees order of execution; produces clean, linear, easy-to-read code. |
| Parallel Await |
const [r1, r2] = await Promise.all([p1, p2]);
|
For multiple independent operations that can be run concurrently. | Drastically improves performance by running tasks in parallel. |
| Error Handling |
try { await p(); } catch (e) { ... }
|
Gracefully handling rejections from awaited Promises. | Uses familiar synchronous error handling syntax; centralizes error logic. |
| Conditional Assignment |
let x; if(c){ x = await p1(); } else { x = await p2();
}
|
Executing different asynchronous operations based on some condition. | Allows complex, dynamic asynchronous workflows with standard control flow. |
| Resilient Parallelism |
const r = await Promise.allSettled([p1, p2]);
|
Running multiple operations where some are expected to fail. | Prevents a single failure from failing the entire set of operations. |
| Racing / Timeouts |
const r = await Promise.race([p, timeout]);
|
Getting the result of the fastest operation or implementing timeouts. | Improves responsiveness and prevents infinitely pending operations. |
Comprehensive Integration Project
Project Brief: You will build a "GitHub User Profile" command-line dashboard. The application will take a GitHub username as an argument and fetch various pieces of data from the GitHub API to display a summary. The challenge is that API requests can be slow or fail, and some data depends on other data being fetched first. Your dashboard must be both fast and resilient, using the best async patterns for each task.
You need to fetch the user's main profile information, their list of public repositories, and for their top 3 repositories (by star count), you need to fetch the list of recent commits. This requires a mix of sequential and parallel operations, as well as robust error handling.
Requirements Checklist:
-
[ ] Must use sequential
awaitto first fetch the user profile before fetching their repos. -
[ ] Must use
const data = await ...to store the results of API calls. -
[ ] Must use
Promise.allto fetch the commit lists for the top 3 repos in parallel. -
[ ] Must use a
try...catchblock to handle cases where the GitHub username does not exist (API returns a 404). -
[ ] Must handle potential failures when fetching commits for a
single repo without stopping the entire application, perhaps by
using
Promise.allSettled. - [ ] Code must be commented to explain why a particular async pattern was chosen (e.g., "Using Promise.all here for performance").
Starter Template:
// A simple fetch wrapper to avoid repetition
async function fetchGitHubAPI(path) {
const url = `https://api.github.com${path}`;
console.log(`Fetching: ${url}`);
const response = await fetch(url, {
headers: { 'Accept': 'application/vnd.github.v3+json' }
});
if (!response.ok) {
throw new Error(`GitHub API Error: ${response.status} for ${url}`);
}
return response.json();
}
async function generateUserProfile(username) {
try {
// 1. Fetch user profile data. This must happen first.
const userProfile = await fetchGitHubAPI(`/users/${username}`);
// 2. Fetch the user's repositories. This depends on the user profile fetch succeeding.
const repos = await fetchGitHubAPI(`/users/${username}/repos`);
console.log(`\n--- User: ${userProfile.name} (${userProfile.login}) ---`);
console.log(`Bio: ${userProfile.bio || 'N/A'}`);
console.log(`Repos: ${userProfile.public_repos}`);
// 3. Find the top 3 repos by star count.
// HINT: Use sort() and slice()
const topRepos = repos
.sort((a, b) => b.stargazers_count - a.stargazers_count)
.slice(0, 3);
console.log(`\n--- Top 3 Repositories ---`);
// 4. Fetch commits for the top 3 repos in parallel.
// HINT: a) Map `topRepos` to an array of promises for their commits.
// b) The commit URL is /repos/{owner}/{repo}/commits
// c) Use Promise.allSettled for resilience.
// 5. Display the results.
} catch (error) {
console.error(`\nError: ${error.message}`);
console.error('Could not generate profile. Please check the username and your network connection.');
}
}
// Get username from command line arguments or use a default
const username = process.argv[2] || 'microsoft';
generateUserProfile(username);
Success Criteria:
- Criterion 1: Correct Sequential Fetching: The application first logs the user's profile info before attempting to fetch or display repo info.
- Criterion 2: Correct Parallel Fetching: The "Fetching" logs for the three commit endpoints appear in quick succession, not one after another with a delay.
-
Criterion 3: Graceful User Not Found Error: If a
non-existent username is provided (e.g.,
node script.js this-user-does-not-exist), thetry...catchblock catches the error and logs a user-friendly message. - Criterion 4: Resilient Commit Fetching: If one of the top 3 repos has protected commits and the API fails, the application should still display commit info for the other two repos successfully.
- Criterion 5: Top Repos Calculated Correctly: The application must correctly identify and display the names of the three repositories with the most stars.
- Criterion 6: Final Output is Formatted: The final output should be readable, clearly separating user info, top repos, and their latest commit messages.
Extension Challenges:
-
Add Caching: Implement a simple in-memory cache
(using a
Map) to store API results. Before anyfetchGitHubAPIcall, check the cache. If the data exists and is less than 5 minutes old, return it from the cache instead of making a network request. - Implement a Rate Limiter: The GitHub API has rate limits. Modify the parallel commit fetching logic to fetch commits in batches of no more than 2 at a time to avoid hitting the rate limit.
-
Add a Timeout: Wrap the API calls in a
withTimeoututility function (as seen in the advanced examples). If any single API request takes longer than 3 seconds, it should fail with a "Timeout" error, which should be handled gracefully.
Connection to Professional JavaScript
In the professional world, async/await is not just a
feature; it's the bedrock of modern JavaScript development, especially
in Node.js backend services and data-heavy frontend applications.
Popular libraries and frameworks are built around it. For instance, in
a backend framework like Express or Fastify, route handlers are often
async functions that await database queries
or calls to other microservices. A request handler might look like
async (req, res) => { const user = await
db.findUser(req.body.id); res.send(user); }. This clean syntax is what makes building complex APIs manageable.
When a professional developer reviews your code, they expect to see a
deep understanding of these patterns. They will look for correct use
of sequential await for dependent tasks and parallel
execution with Promise.all for independent ones as a
baseline for performance. They will scrutinize your error handling,
expecting robust try...catch blocks and the use of
Promise.allSettled for non-critical tasks. Demonstrating
that you not only know the syntax but also understand the
why—the trade-offs between performance, resilience, and
readability—is a key indicator of a senior, production-ready engineer.
These patterns are the language of modern, high-quality JavaScript.