π§© Modern JavaScript Data & Async
Modern JavaScript provides powerful tools for working with data. This module covers ES6+ syntax features, data structures, and the async patterns that make JavaScript unique.
In Module 1, you learned the fundamentals: variables, functions, objects, and control flow. Now we level up. This module introduces the modern ES6+ tools that professional developers use daily β cleaner syntax for working with data (spread, destructuring, template literals), the universal data format of the web (JSON), powerful array transformations, robust error handling, and the async patterns (Promises & async/await) that make JavaScript uniquely suited for web applications.
π― By the end of this module, you will:
- Use spread/rest operators and destructuring for clean data access
- Work with JSON β the language of web APIs
- Master array methods (
map,filter,reduce, β¦) - Handle errors with
try/catch - Understand Promises and write
async/awaitcode
π Spread & Rest Operators
The ... operator does two opposite things depending on where it's used:
function sum(...nums) { }
Math.max(...arr)
π Quick Reference
fn(a, ...rest)REST β Parameters[a, ...rest] = arrREST β Destructure[...arr1, ...arr2]SPREAD β Arrays{...obj, key: val}SPREAD β ObjectsREST: Collecting Multiple β One
The rest operator (...) collects remaining items into a single array. Use it in function parameters to accept any number of arguments, or in destructuring to capture "the rest" of the values you didn't explicitly name.
// REST in function parameters: collect arguments into array
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// REST with named parameters (rest must be last!)
function greet(greeting, ...names) {
return `${greeting} ${names.join(', ')}!`;
}
console.log(greet('Hello', 'Ali', 'Sara', 'Omar'));
// "Hello Ali, Sara, Omar!"
// REST in array destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(rest); // [3, 4, 5]
// REST in object destructuring
const user = { name: 'Mehdi', age: 32, city: 'Casa', role: 'dev' };
const { name, ...otherProps } = user;
console.log(otherProps); // { age: 32, city: 'Casa', role: 'dev' }SPREAD: Expanding One β Multiple
The spread operator (...) does the opposite of rest β it expands an array or object into individual elements. Use it to clone arrays, merge objects, or pass array items as separate function arguments.
// SPREAD in arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
const clone = [...arr1]; // shallow copy
// SPREAD in function calls
const nums = [1, 5, 3, 9, 2];
console.log(Math.max(...nums)); // 9
// SPREAD in objects
const userObj = { name: "Mehdi", age: 32 };
const location = { city: "Casablanca", country: "Morocco" };
const profile = { ...userObj, ...location };
// { name: "Mehdi", age: 32, city: "Casablanca", country: "Morocco" }
// Override properties (later values win)
const updated = { ...userObj, age: 33, role: "admin" };
// { name: "Mehdi", age: 33, role: "admin" }Putting It Together: REST + SPREAD
REST and SPREAD often work together. A function can collect arguments with rest, then expand them with spread inside the function body. This pattern is especially useful for immutable data updates β a core pattern in modern frameworks like React.
// Use both in same function!
function mergeAndLog(label, ...arrays) { // REST: collect arrays
const merged = [].concat(...arrays); // SPREAD: expand each
console.log(`${label}:`, merged);
return merged;
}
mergeAndLog('Numbers', [1, 2], [3, 4], [5, 6]);
// Practical: updating nested objects immutably
const state = {
user: { name: 'Ali', age: 25 },
settings: { theme: 'dark', lang: 'en' }
};
const newState = {
...state, // SPREAD: copy all properties
user: { ...state.user, age: 26 } // SPREAD + override
};
console.log(state.user.age); // 25 (original unchanged)
console.log(newState.user.age); // 26Common Mistake: Rest must be the last parameter. function bad(...first, last) { } is a SyntaxError! Also, you can't spread into nothing: const x = ...arr; β spread must be inside [], {}, or a function call.
π Template Literals
Template literals use backticks (`) and provide a cleaner, more readable way to work with strings compared to concatenation.
Before ES6, building strings with variables meant awkward concatenation: mixing + operators, wrapping values in quotes, and escaping newlines with \n. Template literals replaced all of that with a single, intuitive syntax β backticks and ${} placeholders. They're now the default choice for any string that includes variables, expressions, or multiple lines.
'Hello ' + name + '!'
`Hello ${name}!`
const user = { name: 'Mehdi', email: 'mehdi@example.com' };
const orderCount = 5;
const total = 249.99;
// β CONCATENATION: Hard to read
const msg1 = 'Hello ' + user.name + ',\n' +
'You have ' + orderCount + ' orders.\n' +
'Total: $' + total.toFixed(2);
// β
TEMPLATE LITERAL: Clean, readable
const msg2 = `Hello ${user.name},
You have ${orderCount} orders.
Total: $${total.toFixed(2)}
Email: ${user.email}`;
// Expressions inside ${}
const price = 100;
const tax = 0.2;
console.log(`Total: ${price * (1 + tax)} MAD`); // Total: 120 MAD
// Function calls inside ${}
const loud = (text) => text.toUpperCase();
console.log(`Message: ${loud('hello')}`); // Message: HELLO
// Use Case: HTML Generation
function createUserCard(u) {
return `
<div class="user-card">
<h3>${u.name}</h3>
<p>Email: ${u.email}</p>
<span class="status ${u.active ? 'active' : 'inactive'}">
${u.active ? 'β Active' : 'β Inactive'}
</span>
</div>`;
}When to use each: Template literals for multi-line strings, complex interpolation, HTML/SQL generation. Concatenation for simple single additions like 'Hello' + name. General rule: if you need more than 2 values or multiple lines β use templates.
β οΈ Common Mistakes with Template Literals
- Wrong quotes: Using regular quotes (
'...'or"...") instead of backticks (`...`). Interpolation${}only works inside backticks! - Forgetting
${}: Writing`Hello name`instead of`Hello ${name}`β the variable name becomes a literal string. - Nested backticks: If you need backticks inside a template literal, escape them with
\`or use a different quoting strategy.
π¦ Destructuring
Destructuring lets you extract values from arrays or properties from objects into separate variables β with a single statement.
const { name, age } = obj;
const [ first, second ] = arr;
Object destructuring
const user = { name: "Mehdi", age: 32, city: "Casablanca" };
// Extract properties into variables
const { name, age } = user;
console.log(name); // "Mehdi"
// Rename while destructuring
const { name: userName, city: location } = user;
console.log(userName); // "Mehdi"
console.log(location); // "Casablanca"
// Default values (if property doesn't exist)
const { country = "Morocco", email = "none" } = user;
console.log(country); // "Morocco"Function parameters destructuring
// Destructure in the parameter list
function displayUser({ name, age, city = "Unknown" }) {
console.log(`Name: ${name}, Age: ${age}, City: ${city}`);
}
displayUser({ name: "Youssef", age: 28 });
// Name: Youssef, Age: 28, City: Unknown
// REST in destructuring
const { name, ...rest } = { name: "Ali", age: 25, city: "Fes" };
console.log(rest); // { age: 25, city: "Fes" }Array destructuring
const colors = ["red", "green", "blue", "yellow"];
// Extract by position
const [first, second] = colors;
console.log(first); // "red"
// Skip elements using commas
const [, , third] = colors;
console.log(third); // "blue"
// Swapping variables without temp variable
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1REST in array destructuring
const numbers = [1, 2, 3, 4, 5];
const [first, ...rest] = numbers;
console.log(rest); // [2, 3, 4, 5]
// Function returning multiple values
function getCoordinates() { return [10, 20]; }
const [x, y] = getCoordinates();
// Destructure in loop
const pairs = [[1, 2], [3, 4], [5, 6]];
pairs.forEach(([a, b]) => {
console.log(`a=${a}, b=${b}`);
});Mixed: destructuring arrays of objects
const users = [
{ id: 1, name: "Fatima", role: "admin" },
{ id: 2, name: "Hassan", role: "user" },
{ id: 3, name: "Nadia", role: "moderator" }
];
// Destructure first user's properties
const [{ name: firstName, role }] = users;
console.log(firstName); // "Fatima"
// Map with destructuring
const names = users.map(({ name }) => name);
console.log(names); // ["Fatima", "Hassan", "Nadia"]
// Filter admins using destructuring
const admins = users.filter(({ role }) => role === "admin");When to use destructuring: Function parameters with many options, extracting data from API responses, working with React props/state, swapping variables, cleaning up code that repeatedly accesses obj.property.
π JSON
The Problem: Storing Data in Text Format
Before standardized formats, storing structured data was a programmer's nightmare. Every developer had their own encoding rules.
β οΈ The Pre-Standard Era
π¨βπ» Programmer A: Comma-separated
Alice,25,alice@example.com
Bob,30,bob@example.comβ What if name contains a comma? Adding new fields breaks parsing. No optional fields.
π¨βπ» Programmer B: Fixed-width columns
Alice 25alice@example.com
Bob 30bob@example.comβ Names longer than 10 chars break format. Wastes space with padding.
π¨βπ» Programmer C: Custom delimiters
name:Alice|age:25|email:alice@example.comβ What if data contains | or :? Complex parsing. Custom parser per app.
The Solution: Standardized Formats
<user>
<name>Alice</name>
<age>25</age>
<email>alice@mail.com</email>
</user>Verbose, large files, complex parsing. Good for documents.
{
"name": "Alice",
"age": 25,
"email": "alice@mail.com"
}Lightweight, 30-50% smaller, native JS support, perfect for APIs.
JSON Syntax & Data Types
"hello"42, 3.14true, falsenull[1, 2, 3]{"k":"v"}Working with JSON in JavaScript
JSON.parse() β JSON string β JavaScript object
const jsonString = '{"name":"Alice","age":25,"skills":["JavaScript","Python"]}';
const user = JSON.parse(jsonString);
console.log(user.name); // "Alice"
console.log(user.skills[0]); // "JavaScript"
console.log(typeof user); // "object"JSON.stringify() β JavaScript object β JSON string
const user = { name: "Bob", age: 30, skills: ["React", "Node.js"] };
// Compact
const json = JSON.stringify(user);
// '{"name":"Bob","age":30,"skills":["React","Node.js"]}'
// Pretty-print with indentation
console.log(JSON.stringify(user, null, 2));
// {
// "name": "Bob",
// "age": 30,
// ...
// }Filtering with replacer
const user = { name: "Alice", age: 25, password: "secret123" };
// Array of allowed keys
const safe1 = JSON.stringify(user, ["name", "email"]);
// '{"name":"Alice"}'
// Replacer function for more control
const safe2 = JSON.stringify(user, (key, value) => {
if (key === "password") return undefined; // skip!
return value;
});Handling JSON parse errors
// Safe parsing with default fallback
function safeParse(jsonString, defaultValue = null) {
try {
return JSON.parse(jsonString);
} catch(e) {
return defaultValue;
}
}
const data = safeParse('invalid json', { name: "Default" });
console.log(data); // { name: "Default" }π Real-World JSON Use Cases
π Arrays & Array Methods
Array methods are the Swiss Army knife of JavaScript. Master these and you'll write cleaner, more expressive code.
π Array Methods Decision Guide
.map().filter().reduce().find().forEach().sort()π§ Complete Array Methods Reference
π‘ Hover over any method to see details and examples
.find()
.findIndex()
.findLast()
.findLastIndex()
.indexOf()
.includes()
.some()
.every()
.map()
.filter()
.reduce()
.flatMap()
.flat()
.slice()
.concat()
.push()
.pop()
.shift()
.unshift()
.splice()
.sort()
.reverse()
.toSorted()
.toReversed()
.toSpliced()
.with()
Array.isArray()
Array.from()
.join()
.entries()
.sort()
mutates the original! Use
[...arr].sort()
or
.toSorted()
to preserve original.
Below is a practical example using an array of order objects. Each method serves a specific purpose β map transforms, filter selects, reduce combines, find searches, and some/every check conditions:
const orders = [
{ id: 1, total: 50, status: 'paid' },
{ id: 2, total: 120, status: 'pending' },
{ id: 3, total: 80, status: 'paid' }
];
// Transform β .map()
const ids = orders.map(o => o.id); // [1, 2, 3]
// Select β .filter()
const paid = orders.filter(o => o.status === 'paid'); // 2 items
// Combine β .reduce()
const revenue = paid.reduce((sum, o) => sum + o.total, 0); // 130
// Find β .find()
const order = orders.find(o => o.id === 2); // { id: 2, total: 120, ... }
// Check any β .some()
orders.some(o => o.total > 100); // true
// Check all β .every()
orders.every(o => o.status === 'paid'); // falseThe reduce method deserves special attention β it's the most versatile array method and can implement almost any array transformation. Below you'll also see sort (which mutates the original!) and how to chain multiple methods together for powerful one-line transformations:
// reduce β the most powerful method
const sum = [1, 2, 3, 4].reduce((acc, n) => acc + n, 0); // 10
// reduce for grouping
const people = [
{ name: "Mehdi", dept: "IT" },
{ name: "Ada", dept: "CS" },
{ name: "Alan", dept: "CS" }
];
const grouped = people.reduce((groups, person) => {
const key = person.dept;
groups[key] = groups[key] || [];
groups[key].push(person);
return groups;
}, {});
// { IT: [{...}], CS: [{...}, {...}] }
// sort β β οΈ mutates the original array!
const nums = [10, 2, 5];
console.log([...nums].sort((a, b) => a - b)); // [2, 5, 10]
// Chaining methods
const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter(n => n % 2 === 0) // [2, 4, 6, 8, 10]
.map(n => n ** 2) // [4, 16, 36, 64, 100]
.reduce((sum, n) => sum + n, 0); // 220NB: Methods like forEach, map, filter, sort accept callbacks. They abstract away "how to iterate" so you focus on what to do. This is the practical power of callbacks.
π‘οΈ Error Handling
Use try { ... } catch (err) { ... } finally { ... } to handle exceptions. You can also throw your own errors when inputs are invalid.
π‘οΈ Error Handling Flow
// Basic try/catch
const text = '{"id":1,"name":"Mehdi"}';
try {
const obj = JSON.parse(text);
console.log('Parsed:', obj.name);
} catch (err) {
console.error('Invalid JSON:', err.message);
}
// Throwing your own errors
function validateAge(age) {
if (typeof age !== 'number' || Number.isNaN(age)) {
throw new TypeError('age must be a number');
}
if (age < 0) throw new RangeError('age must be >= 0');
return age;
}
try {
validateAge('x');
} catch (e) {
console.error('Validation error:', e.message);
}The finally block runs no matter what β whether the code succeeded or threw an error. It's the perfect place for cleanup tasks like closing connections, hiding loading spinners, or releasing resources:
// finally for cleanup
function work() {
console.log('start');
try {
JSON.parse('{ bad json');
} catch (e) {
console.error('caught:', e.message);
} finally {
console.log('cleanup always runs');
}
console.log('end');
}
work();β οΈ Heads-up: try/catch only catches errors that happen inside the try block. Errors in setTimeout callbacks run later, outside the try:
// β BAD: try/catch won't catch errors inside setTimeout
try {
setTimeout(() => {
throw new Error('Async boom!'); // Runs later, OUTSIDE the try
}, 100);
} catch (e) {
console.error('Never reached!'); // Won't run
}
// β
GOOD: Catch inside the callback itself
setTimeout(() => {
try {
throw new Error('Async boom!');
} catch (e) {
console.error('Caught inside callback:', e.message);
}
}, 100);
// β
BEST: Use Promises/async-await instead
async function safeAsync() {
try {
const result = await fetchData();
console.log(result);
} catch (e) {
console.error('Caught with async/await:', e.message);
}
}Error Levels: Bubbling to the Nearest catch
When an error occurs, JavaScript looks for the nearest surrounding try/catch up the call stack. If none is found, it reaches the global error handler.
Caught at top level
function c() { throw new Error('boom in c'); }
function b() { c(); } // no catch here
function a() {
try {
b();
console.log('after b'); // not reached
} catch (e) {
console.log('caught in a:', e.message);
}
}
a();Caught closer to the source
function c() { throw new Error('boom in c'); }
function b() {
try { c(); }
catch (e) { console.log('handled in b:', e.message); }
}
function a() {
b();
console.log('a continues'); // reached!
}
a();β‘ Where to Place try/catch
A common beginner mistake is wrapping every single line in its own try/catch. Instead, place your error handling at function boundaries β wrap the entire logical operation, not individual steps. This keeps your code clean and lets errors bubble to the right level:
// β BAD: Catching too granularly
function processUserBad(data) {
let parsed;
try { parsed = JSON.parse(data); }
catch (e) { console.error('Parse error'); return null; }
try { validate(parsed); }
catch (e) { console.error('Validation error'); return null; }
return parsed;
}
// β
GOOD: Catch at function boundary
function processUserGood(data) {
try {
const parsed = JSON.parse(data);
validate(parsed);
save(parsed);
return { success: true, data: parsed };
} catch (error) {
console.error('Failed to process user:', error.message);
return { success: false, error: error.message };
}
}Three levels of error handling: 1) Function boundary β catch at the top level of public functions. 2) Module boundary β catch when crossing module boundaries (API routes, event handlers). 3) Application boundary β global error handler as last resort.
Here's what each level looks like in practice β from the most local (function) to the most global (application-wide handler):
// Level 1: Function boundary (pure function)
function calculateTotal(items) {
try {
return items.reduce((sum, item) => sum + item.price, 0);
} catch (error) {
console.error('Calculation error:', error.message);
return 0; // Safe default
}
}
// Level 2: Module boundary (API route handler)
function handleUserRequest(req, res) {
try {
const user = parseUser(req.body);
const saved = saveToDatabase(user);
res.json({ success: true, user: saved });
} catch (error) {
console.error('[API Error]', error.message);
res.status(400).json({
success: false,
error: 'Failed to create user'
});
}
}
// Level 3: Global error handler (last resort)
window.addEventListener('error', (event) => {
console.error('[Global Error]', event.error);
// Send to error tracking service
});
// Don't catch too early β let errors bubble to the right level!β³ Promises & Async/Await
π The Real-World Problem: Waiting in JavaScript
Imagine ordering pizza online. You don't stare at the screen waiting β you watch TV, do homework. When the pizza arrives, the doorbell rings and you handle it.
JavaScript works the same way! Some operations take time (fetching data, reading files). If JS stopped and waited, your webpage would freeze.
2. Wait... doing nothing...
3. Page frozen...
4. Pizza arrives
5. Finally continue!
2. Watch TV (page responsive!)
3. Do homework (scrolling works!)
4. π Doorbell! Pizza arrives
5. Handle delivery, continue
β±οΈ Synchronous vs Asynchronous
100ms + 100ms + 100ms = 300ms SLOW! βmax(100ms, 100ms, 100ms) = 100ms FAST! β
π― From Callbacks to Async/Await
- Callbacks (original) β functions passed as arguments, but leads to "pyramid of doom"
- Promises (ES2015) β objects representing future values, chainable with
.then() - Async/Await (ES2017) β syntactic sugar over promises, looks like synchronous code
We'll explore each one, see their problems, and understand why we moved to the next solution.
1οΈβ£ Callbacks: The Original Async Pattern
A callback is a function you pass as an argument to another function, to be called later when something finishes.
Simple callback example: setTimeout
setTimeout is the simplest built-in async function in JavaScript. It takes a callback function and a delay in milliseconds, then runs the callback after the delay. Pay close attention to the output order β it reveals how JavaScript handles async code:
// The simplest async function: setTimeout
console.log('1. Ordering pizza...');
setTimeout(function() {
console.log('3. π Pizza delivered!');
}, 2000); // Wait 2 seconds (2000ms)
console.log('2. Doing other stuff while waiting...');
// Output order:
// 1. Ordering pizza...
// 2. Doing other stuff while waiting...
// (2 second pause)
// 3. π Pizza delivered!π‘ Key Insight: Notice the output order! Line 2 runs before line 3, even though line 3 is written first. That's because setTimeout schedules the callback for later β JavaScript doesn't wait, it continues executing the next line immediately.
The Problem: Callback Hell (Pyramid of Doom)
When you need multiple async operations that depend on each other, callbacks get nested deeper and deeper.
Dependency chain: In the flow below β fetch users β user details β user posts β comments β each step depends on the previous step's result, so you can't start the next step until the prior callback finishes. This forces nesting with plain callbacks.
// Pyramid of doom: callback inside callback inside callbackβ¦
// Each API expects a callback: (err, data)
fetchUsers(function (err, users) {
if (err) return console.error(err);
fetchUserDetails(users[0].id, function (err, details) {
if (err) return console.error(err);
fetchUserPosts(users[0].id, function (err, posts) {
if (err) return console.error(err);
fetchPostComments(posts[0].id, function (err, comments) {
if (err) return console.error(err);
console.log('done:', { details, comments });
});
});
});
});Problems with callbacks: Deep nesting hard to read, error handling repeated everywhere (if (err)), hard to reason about flow, difficult to debug.
π From Callbacks to Promises
Now let's see how the exact same task (users β details β posts β comments) becomes cleaner with promises. No more nesting!
// The same 4-step chain, but with promises β flat and readable!
fetchUsers()
.then(users => {
console.log('Step 1: Got users');
return fetchUserDetails(users[0].id);
})
.then(details => {
console.log('Step 2: Got details');
return fetchUserPosts(details.userId);
})
.then(posts => {
console.log('Step 3: Got posts');
return fetchPostComments(posts[0].id);
})
.then(comments => {
console.log('Step 4: Got comments');
console.log('Done!', comments);
})
.catch(err => {
// ONE catch handles errors from ANY step above!
console.error('Error:', err.message);
});β
Compare: 4 levels of nesting became a flat chain. Error handling went from 4 repeated if (err) checks to a single .catch(). Same logic, much more readable!
2οΈβ£ Promises: A Better Way
A Promise is an object representing a value that will be available in the future β a "receipt" for async work. It's either pending, fulfilled (success), or rejected (error).
β¨ Why Use Promises? Key Advantages
Flatten nested callbacks into a readable chain: .then().then().then() β linear and easy to follow.
The same Promise can be consumed with .then() for async callback-style or with modern syntax (covered next). The API doesn't force a style on you!
Promise-based code is always non-blocking. The event loop continues running β UI stays responsive, other code executes while waiting for results.
One .catch() at the end handles errors from any step. No more repetitive if (err) in every callback.
- β Fulfilled: Pizza arrives! (success)
- β Rejected: Restaurant is closed. (error)
A Promise is always in one of three states. It starts as pending, and eventually transitions to either fulfilled (success) or rejected (failure). Once it transitions, it's called settled β and it can never change state again. This is a key guarantee: a promise settles exactly once.
The initial state of every promise. The asynchronous operation is still running β the result is not yet available. During this phase, .then() and .catch() handlers are registered but not called. The promise is waiting for either resolve() or reject() to be called inside the executor.
The operation completed successfully. The resolve(value) function was called, and the promise now carries a result value. All .then(onFulfilled) handlers run with that value. If the promise is already fulfilled when .then() is attached, the handler still runs (asynchronously, in the microtask queue).
The operation failed. The reject(error) function was called (or an exception was thrown inside the executor). All .catch(onRejected) handlers run with the error. If no .catch() is attached, you'll see an UnhandledPromiseRejection warning β always handle rejections!
π "Settled" = Final State
Both fulfilled and rejected are called settled states. Once a promise settles, its value or error is locked in forever β calling resolve() or reject() again has no effect. This immutability is what makes promises safe to pass around: multiple consumers can attach .then() handlers without worrying about the state changing.
π Promise Syntax β Step by Step
Step 1: Creating a Promise
To create a Promise, you use new Promise() and pass it an executor function with two parameters: resolve (call when successful) and reject (call when something fails). The promise starts in a pending state until one of them is called:
// Creating a promise that will succeed after 1 second
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success! π'); // Call resolve() when successful
}, 1000);
});
console.log('Promise created:', myPromise); // Promise { <pending> }Key concepts:
new Promise()β Creates a new promise objectresolveβ Function to call when the task succeedsrejectβ Function to call when the task fails- The promise starts in pending state
Step 2: Using the Promise with .then()
Once you have a promise, you consume it with .then(). The callback inside .then() receives the resolved value and runs only when the promise fulfills:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => resolve('Data loaded!'), 1000);
});
// .then() runs when the promise resolves
myPromise.then((result) => {
console.log('Got result:', result); // Runs after 1 second
});
console.log('Promise started, waiting...');Step 3: Handling Errors with .catch()
When a promise calls reject() instead of resolve(), the .then() callback is skipped and the error flows to .catch(). This is how you handle failures gracefully:
const failingPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Something went wrong! β'));
}, 1000);
});
failingPromise
.then((result) => {
console.log('Success:', result); // Won't run
})
.catch((error) => {
console.error('Caught error:', error.message); // Runs!
});Putting It All Together
Now letβs combine all three concepts β creating a promise, consuming it with .then(), catching errors with .catch(), and cleaning up with .finally() β into a single, complete example:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('Data loaded!'); // β fulfilled
} else {
reject(new Error('Failed')); // β rejected
}
}, 1000);
});
// Consuming with .then() / .catch() / .finally()
myPromise
.then(data => console.log(data)) // "Data loaded!"
.catch(err => console.error(err))
.finally(() => console.log("Done")); // always runsError Propagation
One of the best features: one .catch() at the end handles errors from ANY step!
getUser(id)
.then(user => {
console.log('Step 1: Got user');
return getProfile(user); // This might fail
})
.then(profile => {
console.log('Step 2: Got profile'); // Skipped if step 1 fails
return getSettings(profile);
})
.then(settings => {
console.log('Step 3: Got settings');
})
.catch(err => {
// This ONE catch handles errors from ANY step above!
console.error('Error:', err.message);
});π Key Point: Errors "bubble up" through the chain. If step 2 fails, steps 3, 4, 5 all get skipped and the error goes straight to .catch(). Much cleaner than callbacks!
π Interactive Promise Lifecycle Diagram
Use the controls below to see how promises move through different states:
π Code Examples
// Promise resolves β .then() receives the value
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('Success! Data loaded'), 1000);
});
promise.then(value => {
console.log(value); // "Success! Data loaded"
return value.toUpperCase();
})
.then(transformed => {
console.log(transformed); // "SUCCESS! DATA LOADED"
});// Promise rejects β .catch() handles the error
const promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Network timeout')), 1000);
});
promise.then(value => {
console.log('This will not run');
})
.catch(error => {
console.error('Caught:', error.message); // "Network timeout"
});// .catch() recovers: transforms error β success
const promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('API failed')), 1000);
});
promise
.then(value => value) // Skipped!
.catch(error => {
console.error('Caught:', error.message);
return 'Using cached data'; // Recovery!
})
.then(data => {
console.log('Continues:', data); // "Using cached data"
});// Error in .then() jumps to .catch()
const promise = new Promise((resolve) => {
setTimeout(() => resolve({ id: 42, name: 'Alice' }), 500);
});
promise.then(user => {
console.log('User received:', user);
if (user.id < 100) {
throw new Error('User ID too low');
}
return user;
})
.catch(error => {
console.error('Caught:', error.message); // "User ID too low"
});// Chaining: .then() returns a promise β waits for it
function fetchUser(id) {
return new Promise(resolve => {
setTimeout(() => resolve({ id, name: 'Bob' }), 300);
});
}
function fetchUserPosts(userId) {
return new Promise(resolve => {
setTimeout(() => resolve(['Post 1', 'Post 2']), 400);
});
}
fetchUser(123).then(user => {
console.log('Got user:', user);
return fetchUserPosts(user.id); // Returns new promise!
})
.then(posts => {
console.log('Got posts:', posts);
});// .finally() always runs β perfect for cleanup
let isLoading = true;
fetchData()
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error.message))
.finally(() => {
isLoading = false; // Cleanup always happens
console.log('Loading:', isLoading); // false
});β‘ Promise Static Methods
Besides creating promises with new Promise(), JavaScript provides two handy shortcut methods for creating promises that are already settled β useful for early returns, testing, and ensuring consistent return types:
β
Promise.resolve(value)
Creates a promise already fulfilled. Useful for ensuring a function always returns a Promise.
const p = Promise.resolve('Hello');
p.then(v => console.log(v)); // "Hello"
// Ensuring consistent return type
function getConfig(useCache) {
if (useCache) return Promise.resolve({ theme: 'dark' });
return fetch('/api/config').then(r => r.json());
}β Promise.reject(reason)
Creates a promise already rejected. Great for early validation/exit.
function getUserById(id) {
if (!id || typeof id !== 'number')
return Promise.reject(new Error('Invalid user ID'));
return fetch(`/api/users/${id}`).then(r => r.json());
}
getUserById(-5)
.catch(err => console.error(err.message));π‘ Key Points:
- Immediate settlement: Both methods create promises that are already settled (no async work happens)
- Microtask queue: Even though settled immediately,
.then()/.catch()callbacks still run asynchronously (in the next microtask) - Passing a Promise: If you pass a Promise to
Promise.resolve(), it returns that same promise unchanged
3οΈβ£ Async/Await: The Modern Way
Async/await is syntactic sugar over promises that makes asynchronous code look and behave like synchronous code. It's the modern, preferred way to work with promises.
Basic Usage Example
Let's start with a practical example to see how async/await works:
// Regular promise-based function
function fetchUserData(userId) {
return new Promise(resolve => {
setTimeout(() => resolve({ id: userId, name: 'Alice' }), 1000);
});
}
// Using async/await (modern way)
async function getUserInfo(userId) {
console.log('Fetching user...');
const user = await fetchUserData(userId); // Wait for promise to resolve
console.log('User:', user);
return user; // Automatically wrapped in a Promise!
}
getUserInfo(123);Understanding the async Keyword
Adding async before a function declaration does one important thing: it makes the function always return a Promise. Even if you return a plain value like a string, JavaScript automatically wraps it in Promise.resolve(). Here are the three rules to remember:
// Rule 1: async function ALWAYS returns a Promise
async function example1() {
return 'Hello'; // Automatically wrapped in Promise.resolve('Hello')
}
example1().then(result => console.log(result)); // "Hello"
// Rule 2: Even explicit values become promises
async function example2() {
return Promise.resolve(42);
}
example2().then(result => console.log(result)); // 42
// Rule 3: async without await is unnecessary (but valid)
async function example3() {
console.log('No await here'); // Just a normal function that returns Promise
return 'Done';
}
example3();Understanding the await Keyword
The await keyword pauses the execution of an async function until the awaited Promise resolves. It can only be used inside an async function (or at the top level of ES modules). Think of it as saying "wait here for the result before continuing to the next line":
// Rule 1: await can ONLY be used inside async functions
async function mustBeAsync() {
const result = await Promise.resolve('Valid');
console.log(result); // "Valid"
}
// β This would be an ERROR (uncomment to see):
// function notAsync() {
// const result = await Promise.resolve('Invalid'); // SyntaxError!
// }
// Rule 2: await pauses the function until Promise resolves
async function sequential() {
console.log('Start');
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('After 1 second'); // Waits here
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('After 2 seconds'); // Waits here too
}
sequential();
// Rule 3: You can have async without await (but not await without async)
async function noAwait() {
return 'No await needed here!'; // Still valid
}
noAwait().then(console.log);Error Handling with try/catch
With async/await, error handling becomes intuitive β you use the same try/catch blocks you already know from synchronous code. If an awaited promise rejects, the error is thrown and caught by the catch block, just like a regular exception:
function fetchData(shouldFail) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error('API failed!'));
} else {
resolve({ data: 'Success!' });
}
}, 500);
});
}
// Modern error handling with try/catch
async function handleData(shouldFail) {
try {
const result = await fetchData(shouldFail);
console.log('Got data:', result);
return result;
} catch (error) {
console.error('Error caught:', error.message);
return { data: 'Fallback data' }; // Recovery!
}
}
// Test both success and failure
handleData(false); // Success
handleData(true); // Error caught and handledasyncfunction always returns a Promiseawaitcan only be used inside async functions (or at top-level in ES modules since ES2022)asyncwithoutawaitis valid but usually unnecessarytry/catchfor error handling (cleaner than.catch())- Locally blocking, globally non-blocking!
awaitpauses only its async function, but other code/events continue running
β‘ Promise Combinators
Sequential vs Parallel Execution
One of the most impactful performance decisions in async code is whether to run operations sequentially (one after another) or in parallel (all at once). When tasks are independent of each other, running them in parallel can dramatically reduce total execution time:
// β SLOW: Sequential with await (3 seconds total)
async function sequentialFetch() {
const user1 = await fetchUser(1); // Wait 1s
const user2 = await fetchUser(2); // Wait 1s
const user3 = await fetchUser(3); // Wait 1s
// Total: ~3000ms
}
// β
FAST: Parallel with Promise.all (1 second total)
async function parallelFetch() {
const users = await Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]);
// Total: ~1000ms (3x faster!)
}β οΈ Performance Trap: Using await in a loop or sequentially for independent operations is slow! If tasks don't depend on each other, use Promise.all() to run them concurrently.
Promise.all(promiseArray) β Wait for All
Takes: An array of promises
Returns: A promise that resolves with an array of all resolved values (same order)
Behavior: Runs all promises concurrently. If any rejects, the entire operation fails immediately.
function apiCall(id, delay) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 3) reject(new Error(`API ${id} failed!`));
else resolve({ id, data: `Result ${id}` });
}, delay);
});
}
async function usePromiseAll() {
try {
const results = await Promise.all([
apiCall(1, 500),
apiCall(2, 1000),
// apiCall(3, 200) // Uncomment to see it fail
]);
console.log('All succeeded:', results);
} catch (error) {
console.error('One failed, all rejected:', error.message);
}
}
usePromiseAll();Promise.allSettled(promiseArray) β Get All Results
Takes: An array of promises
Returns: A promise with an array of result objects: {status: 'fulfilled', value} or {status: 'rejected', reason}
Behavior: Runs all concurrently. Never rejects β waits for all to finish regardless of success/failure.
async function usePromiseAllSettled() {
const results = await Promise.allSettled([
Promise.resolve('Success 1'),
Promise.reject(new Error('Failed 2')),
Promise.resolve('Success 3')
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled')
console.log(`${i}: β
`, result.value);
else
console.log(`${i}: β`, result.reason.message);
});
}
usePromiseAllSettled();Promise.race(promiseArray) β First One Wins
Takes: An array of promises
Returns: A promise that resolves/rejects with the first settled promise's value/reason
Behavior: Returns as soon as ANY promise settles (resolve OR reject). Others are ignored.
function slowAPI() {
return new Promise(resolve =>
setTimeout(() => resolve('Slow API (2s)'), 2000)
);
}
function fastAPI() {
return new Promise(resolve =>
setTimeout(() => resolve('Fast API (500ms)'), 500)
);
}
// First to finish wins!
async function usePromiseRace() {
const winner = await Promise.race([slowAPI(), fastAPI()]);
console.log('Winner:', winner); // "Fast API (500ms)"
}
// π‘ Common pattern: Timeout with Promise.race
async function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout!')), ms)
);
return Promise.race([promise, timeout]);
}
// Test: slowAPI takes 2s, but timeout is 1s
withTimeout(slowAPI(), 1000)
.then(result => console.log('Success:', result))
.catch(error => console.error(error.message)); // "Timeout!"
usePromiseRace();Promise.any(promiseArray) β First Success Wins
Takes: An array of promises
Returns: A promise that resolves with the first fulfilled promise's value
Behavior: Returns as soon as ANY promise fulfills. Ignores rejections unless ALL reject (then throws AggregateError).
async function usePromiseAny() {
try {
const result = await Promise.any([
Promise.reject(new Error('Failed 1')),
Promise.resolve('Success 2'),
Promise.reject(new Error('Failed 3'))
]);
console.log('First success:', result); // "Success 2"
} catch (error) {
// AggregateError β only if ALL promises reject
console.error('All failed:', error);
}
}
usePromiseAny();π Combinators Quick Comparison
Promise.all()β Waits for all, fails if any fails β‘ Best for: All tasks must succeedPromise.allSettled()β Waits for all, never fails π Best for: Need all results regardlessPromise.race()β First to finish (success or failure) π Best for: Timeouts, fastest responsePromise.any()β First success wins π― Best for: Fallback APIs, redundancy
π¬ Microtask Queue: Promise callbacks (.then(), .catch(), .finally()) run in the microtask queue, which has higher priority than regular tasks like setTimeout. This means promise handlers always execute before any pending timers, even if the timer was set first.
π Callbacks vs Promises vs Async/Await
Now that we've covered all three async patterns, let's compare them side by side. This table highlights the key differences to help you decide which pattern to use in different situations:
if (err) in every callback.catch() for entire chaintry/catch block (most readable).then() chainsPromise.all([p1, p2, p3])await Promise.all([p1, p2, p3]).then().then().then()const a = await x; const b = await y;β οΈ Common Mistakes to Avoid
- Forgetting
returnin.then():// β BAD - forgot to return promise promise.then(x => { fetchNext(x); }) // Promise lost! .then(y => console.log(y)); // y is undefined // β GOOD - returns promise so chain waits promise.then(x => fetchNext(x)) // Implicit return .then(y => console.log(y)); - Sequential
awaitwhen parallel is possible:// β SLOW - waits 200ms + 200ms = 400ms const a = await fetchA(); // 200ms const b = await fetchB(); // 200ms // β FAST - waits max(200ms, 200ms) = 200ms const [a, b] = await Promise.all([fetchA(), fetchB()]); - Not handling errors:
// β BAD - unhandled rejection! async function bad() { const data = await fetchData(); // If this fails, app crashes } // β GOOD async function good() { try { const data = await fetchData(); } catch (error) { console.error('Handled:', error); } } - Using
asyncwithoutawait: If you don't useawaitinside anasyncfunction, you don't needasync! - Forgetting
awaitkeyword:// β BAD - missing await, user is a Promise object! const user = fetchUser(); // Promise { pending } // β GOOD const user = await fetchUser(); // { id: 1, name: 'Alice' }
π― When to Use What?
Synchronous code runs top-to-bottom and blocks on each step. Asynchronous code schedules work and continues; results arrive later via callbacks or promises. async/await makes promise code read like sync, but it's still non-blocking β the event loop continues, UI stays responsive.
π Summary
This module covered the tools you'll use every day as a JavaScript developer. You now know how to spread and destructure data cleanly, format strings with template literals, exchange data with APIs using JSON, transform arrays with functional methods, handle errors gracefully with try/catch, and write non-blocking code with Promises and async/await. These aren't just nice-to-know features β they're the foundation of every modern JavaScript codebase. In the next module, you'll apply these skills to the Browser & DOM.
π Spread/Rest
... expands (spread) or collects (rest). Shallow copy, merge, collect args.
π Template Literals
Backticks + ${expr}. Multi-line strings, string interpolation.
π¦ Destructuring
Extract values from arrays/objects. Rename, defaults, nested.
π JSON
stringify / parse. Universal data format for APIs.
π Array Methods
map, filter, reduce, find. Chain them for powerful data transforms.
π‘οΈ Error Handling
try/catch/finally. Throw custom errors. Catch at function boundaries.
π Callbacks
The original async pattern. Leads to callback hell β avoid for new code.
β³ Promises & Async/Await
Pending β Fulfilled/Rejected. async/await for readable async code. Promise.all() for parallel.