DevPath · Learn to code ESPTEN

Asynchrony: promises and async/await

Asynchronous errors

The error that "flies" at another moment

Handling errors in asynchronous code is different because the failure doesn't happen on the line where you wrote the call, but later, when the promise resolves. There are two main tools.

1. try/catch with await

Inside an async function, await turns a rejection into an exception that you can catch with a normal try...catch, as if it were synchronous code:

async function load() {
  try {
    const data = await requestData(); // if it rejects, it jumps to the catch
    return data;
  } catch (error) {
    console.log("Failed:", error.message);
    return null; // default value
  }
}

2. chained .catch()

Without async/await, you use .catch() at the end of the chain:

requestData()
  .then((data) => use(data))
  .catch((error) => console.log("Failed:", error.message));

The classic mistake: try/catch WITHOUT await

This does NOT work the way you expect:

async function bad() {
  try {
    requestData(); // the await is missing!
  } catch (error) {
    console.log("This never runs");
  }
}

Without await, the line requestData() only creates the promise and moves on; the try already finished when, much later, the promise rejects. The synchronous catch can't catch a failure that happens outside its lifetime. It's like closing the safety net before the trapeze artist jumps.

Rule: a try/catch only catches a promise's rejection if you put await in front of it (or if you return the promise and handle the error with .catch).

Uncaught rejections

If a promise rejects and no one handles it (neither await in a try, nor a .catch), an unhandled rejection occurs. In the browser you'll see a warning in the console; in Node, by default, it terminates the process. That's why every promise that can fail should have its error handler.

// ❌ Unhandled rejection: warning / crashed process
Promise.reject(new Error("no one catches me"));

// ✅ Handled
Promise.reject(new Error("I'm caught now")).catch((e) => console.log(e.message));

Examples

try/catch with await returns a default value

function fail() {
  return Promise.reject(new Error("boom"));
}
async function safe() {
  try {
    return await fail();
  } catch (e) {
    return "default value";
  }
}
safe().then((v) => console.log(v)); // "default value"

Without await, the catch does NOT catch the rejection

function fail() {
  return Promise.reject(new Error("boom"));
}
async function bad() {
  try {
    const p = fail(); // without await
    p.catch(() => {}); // we silence it so the demo doesn't break
    return "the try finished without catching anything";
  } catch (e) {
    return "this does NOT run";
  }
}
bad().then((v) => console.log(v));
Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
← Promise combinatorsView the module →