Q1What does invoices.filter((invoice) => invoice.id === "B-302") return?
Syntax Recap — Array Methods, Async Code, and Exceptions at a Glance
Quick-reference tables for array methods by what they return, callbacks vs then vs await, and where each kind of failure gets caught.
Adding up a list of invoices, waiting for an upload to finish, catching a failure and showing a message — each of these can be written in several ways. Choose the wrong one, and you might get undefined where you expected an array, or a failure that never reaches any catch.
This article covers when to use which array method, which way to wait for async work, and how to catch exceptions.
Choosing by the Result You Want — What Array Methods Return
Say you want three things from a list of invoices: how many are paid, the first unpaid one, and the total amount. Array methods are all called the same way, with a callback, so you can't tell just by looking at a call whether you'll get an array or a single value back.
Here, methods are grouped by the shape of their return value: a new array, one of the elements that was in the array, or a value computed from the array, such as a boolean or a total. Once you know the shape, you also know whether you can call join on the result or read a property like .id from it.
const invoices = [
{ id: "B-301", amount: 4800, paid: true },
{ id: "B-302", amount: 1200, paid: false },
{ id: "B-303", amount: 3600, paid: true },
{ id: "B-304", amount: 2500, paid: false },
];
// Returns an array: map keeps the same count, filter keeps only the matches
console.log(invoices.map((invoice) => invoice.id).join(", ")); // B-301, B-302, B-303, B-304
console.log(invoices.filter((invoice) => invoice.paid).length); // 2
// Returns the element itself: the first one that matches
console.log(invoices.find((invoice) => !invoice.paid).id); // B-302
// Returns a value computed from the array: some gives a boolean, reduce gives the folded value
console.log(invoices.some((invoice) => invoice.amount >= 4000)); // true
console.log(invoices.reduce((total, invoice) => total + invoice.amount, 0)); // 12100
If you use filter when you only want one item, you get back an array with one item in it, so you have to add [0] before reading .id. The table below lists the callback-taking array methods covered in this course, grouped by what they return.
| Method | Returns | Watch out for |
|---|---|---|
| map | A new array (same count) | The original array doesn't change |
| filter | A new array (matches only) | An empty array if nothing matches |
| find / findIndex | An element / an index | undefined / -1 if not found |
| some / every | A boolean | false / true on an empty array |
| reduce | What the last callback call returned | An empty array needs an initial value |
| sort / toSorted | A sorted array | sort also changes the original array |
When You Can Keep Chaining — Method Chains and forEach
Say you want the total of only the unpaid invoices. If you store the result of filter in a variable, call map on that variable, and then call reduce, you end up naming in-between arrays you never use again. And if you try to write it all in one expression without knowing which methods can follow which, you'll hit a TypeError where two of them meet.
In chaining (calling the next method on the return value of the previous one), each call like .filter(...) counts as one step, and each step is called on whatever the step before it returned. You can only keep calling array methods when the previous step returned an array.
// invoices is the same 4 invoices as in the previous section
// As long as an array comes back, you can keep adding array methods
const unpaidTotal = invoices
.filter((invoice) => !invoice.paid) // Array: the 2 invoices B-302 and B-304
.map((invoice) => invoice.amount) // Array: [1200, 2500]
.reduce((total, amount) => total + amount, 0); // Number: 3700
console.log(unpaidTotal); // 3700
// toSorted also returns an array, so you can continue on to map and join
const idsByAmount = invoices.toSorted((a, b) => b.amount - a.amount).map((invoice) => invoice.id);
console.log(idsByAmount.join(", ")); // B-301, B-303, B-304, B-302
// forEach returns undefined, so anything chained after it fails
invoices.forEach((invoice) => console.log(invoice.id)).map((invoice) => invoice.id);
// After printing B-301 through B-304
// TypeError: Cannot read properties of undefined (reading 'map')
Put any step that doesn't return an array at the end of the chain. After find, you can read a property like .id but can't call map, and the number reduce returns is simply stored in a variable.
Calling map After find Gives a Different Message
If you chain .map(...) onto the element find returned or the number reduce returned, it stops with a TypeError such as invoices.find(...).map is not a function. The message differs from the one after forEach, but in both cases the step just before .map didn't return an array.
Comparing Ways to Wait — Callbacks / then / await
Say you upload a photo and then create a thumbnail of it. Both take time before their results arrive, so the functions can't hand back their results as return values, and the second step has to wait until the first step's result arrives.
In the code below, ① uses the callback style (you pass the function that starts the work a second function, which it calls when it's done; no Promise is returned), which is how setTimeout works too. ② passes a function to then on the Promise it gets back, and ③ uses await inside an async function.
// ① Callback style: pass a function to call when done, and write the next step inside it
// later calls callback with value after 100ms (it doesn't return a Promise)
function later(value, callback) {
setTimeout(() => callback(value), 100);
}
later("photo-17.jpg", (fileName) => {
later(`a thumbnail of ${fileName}`, (thumbnail) => {
console.log(`① Created ${thumbnail}`); // ① Created a thumbnail of photo-17.jpg
});
});
// ② then: connect the next step with .then (delay is the same as in the Promise article)
delay(100, "photo-17.jpg")
.then((fileName) => delay(100, `a thumbnail of ${fileName}`))
.then((thumbnail) => console.log(`② Created ${thumbnail}`)); // ② Created a thumbnail of photo-17.jpg
// ③ await: write the next step on the line below and get the result in a variable (assumes it runs inside an async function, as in the previous article)
const fileName = await delay(100, "photo-17.jpg");
const thumbnail = await delay(100, `a thumbnail of ${fileName}`);
console.log(`③ Created ${thumbnail}`); // ③ Created a thumbnail of photo-17.jpg
- Call
later("photo-17.jpg", (fileName) => { … })
- The first result arrives in
fileName - Call the second
laterhere
- The second result arrives in
thumbnail - The output goes in the innermost function
- Start from
delay(100, "photo-17.jpg")
- Receives
fileNameand returns the nextdelay
- Receives
thumbnailand prints it
const fileName = await delay(…)const thumbnail = await delay(…)- Write
console.log(…)on the next line
The function in the second then only receives thumbnail, so to use fileName there as well, you'd have to copy it into an outer variable. With await, fileName sits in a variable that any line below can read. The table below puts the three styles side by side.
| Style | Where the result arrives | When you add more steps |
|---|---|---|
| Callback style | An argument of the function you passed | Each step nests one level deeper |
| Passing a function to then | An argument of the function passed to then | Steps connect at the same depth |
| Waiting with await | The variable on the left | Each step goes on the next line |
Checking Where a Failure Lands — try / catch and .catch
Say a function that reserves stock fails when the order quantity exceeds what's in stock. For a regular function that doesn't return a Promise, you can catch the failure by wrapping the calling line in try / catch, but a failure from a function that returns a Promise may or may not reach the catch block, depending on how you write the call.
The catch block that follows try receives exceptions thrown on the lines it wraps. A Promise's .catch calls the function you passed when that Promise is rejected.
function checkStock(count) {
if (count > 3) {
throw new Error(`Not enough stock: ${count} items`);
}
}
async function reserveStock(count) {
await delay(100); // delay is the same as in the Promise article
checkStock(count); // A throw here rejects the Promise this function returns
}
// ① A function that doesn't return a Promise: wrap the calling line in try
try { checkStock(5); } catch (error) { console.log(`① ${error.message}`); }
// ② The then style: pass a function to .catch to receive it
reserveStock(5).catch((error) => console.log(`② ${error.message}`));
// ③ The await style: wrap the await line in try
try { await reserveStock(5); } catch (error) { console.log(`③ ${error.message}`); }
// ① Not enough stock: 5 items
// ② Not enough stock: 5 items (after 100ms)
// ③ Not enough stock: 5 items (after 100ms)
Whether you receive it with .catch or a catch block, the same Error arrives, so error.message and instanceof checks work either way. The table below sums up the ways of handling exceptions covered in this course.
| What you write | What it does | Watch out for |
|---|---|---|
| try / catch | Receives exceptions from the lines it wraps | An empty catch hides failures |
| finally | Runs last, whether or not it succeeded | Put cleanup here |
| throw new Error | Throws an exception right there | new Error alone doesn't stop anything |
| class extends Error | A type you can tell apart with instanceof | Check your own types before Error |
| Error's cause | Attaches the original exception | Read it with error.cause |
| A Promise's .catch | Called when the Promise is rejected | Add it at the end of the then chain |
Knowledge Check
Answer each question one by one.
Q2In invoices.filter(...).map(...).reduce(...), which array does reduce fold?
Q3When you chain three async steps in the callback style, where do you write the third step?