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
Same Array, Different Return Shapes
invoices4 invoices.filterchecks paid.findchecks !paid.reduceadds up amountArray (2 items)B-301 and B-303One elementinvoice B-302One number12100Then.map or .joinThen.id or .amountStraight toconsole.log
The result is an array, an element, or a number, and that changes what you can write after it. Once you know what shape of result you want, the choice of method narrows down.

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.

MethodReturnsWatch out for
mapA new array (same count)The original array doesn't change
filterA new array (matches only)An empty array if nothing matches
find / findIndexAn element / an indexundefined / -1 if not found
some / everyA booleanfalse / true on an empty array
reduceWhat the last callback call returnedAn empty array needs an initial value
sort / toSortedA sorted arraysort 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')
You Can Chain Only While an Array Comes Back
invoicesarray of 4.filter(...)array of 2.map(...)[1200, 2500].reduce(...)3700invoicesarray of 4.forEach(...)undefined.map(...)on undefinedStops witha TypeError
In the top row, each step returns an array until reduce returns a number. forEach returns undefined, so the chain breaks at the next step.

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
How Deeply Each Next Step Is Nested
① Callback style — pass a function to later
  • Call later("photo-17.jpg", (fileName) => { … })
Inside (fileName) => { … }
  • The first result arrives in fileName
  • Call the second later here
Inside (thumbnail) => { … }
  • The second result arrives in thumbnail
  • The output goes in the innermost function
② then — pass a function to .then
  • Start from delay(100, "photo-17.jpg")
Inside the function passed to the first .then
  • Receives fileName and returns the next delay
Inside the function passed to the second .then
  • Receives thumbnail and prints it
③ await — get the result in a variable
  • const fileName = await delay(…)
  • const thumbnail = await delay(…)
  • Write console.log(…) on the next line
Only with ①'s callback style does the next function go inside the function that receives the result. ③'s await creates no functions; the steps are just lines from top to bottom.

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.

StyleWhere the result arrivesWhen you add more steps
Callback styleAn argument of the function you passedEach step nests one level deeper
Passing a function to thenAn argument of the function passed to thenSteps connect at the same depth
Waiting with awaitThe variable on the leftEach 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)
Three Places That Receive the Same Failure
checkStock throwsover 3 items① checkStock(5)called directly② reserveStock(5).catch(...)③ awaitreserveStock(5)The callingline throwsReturned Promiseis rejectedThe awaitline throwscatch blockafter tryArgument of the.catch functioncatch blockafter try
In ②, the calling line doesn't throw; instead, the returned Promise is rejected. A catch block only receives ① and ③, where the line itself throws.

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 writeWhat it doesWatch out for
try / catchReceives exceptions from the lines it wrapsAn empty catch hides failures
finallyRuns last, whether or not it succeededPut cleanup here
throw new ErrorThrows an exception right therenew Error alone doesn't stop anything
class extends ErrorA type you can tell apart with instanceofCheck your own types before Error
Error's causeAttaches the original exceptionRead it with error.cause
A Promise's .catchCalled when the Promise is rejectedAdd it at the end of the then chain
QUIZ

Knowledge Check

Answer each question one by one.

Q1What does invoices.filter((invoice) => invoice.id === "B-302") return?

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?