Error handling for chained ES6 Promises and multiple ES7 async/await

This post is about how to do error handling for multiple async calls in JavaScript. This may seem a simple topic, but I’d often struggled with this when I first learned about JavaScript.

Before doing so, I’d like to quickly go over chained Promises and async/await in JavaScript.

Chained Promise calls

const chainedPromises = () => {
  let animals= [];
  return axios({ method: 'get', url: '/dogs' })
    .then(result1 => {
      animals = animals.concat(result1.data);
      return axios({ method: 'get', url: '/cats' });
    })
    .then(result2 => {
      return animals.concat(result2.data);
    })
    .catch(error1 => {
      console.log('error1:', error1);
    })
    .catch(error2 => {
      // This catch method won't be used.
      // Errors of both the 1st and 2nd axios will be printed above!
      console.log('error2:', error2);
    });
};

(“Why don’t you use Promise.all()?” Maybe that’s what you’re thinking, but that’s not the topic today)

If the first axios (i.e. REST API call) fails, you’ll see the error message in error1, and result1 and result2 won’t be used because the first Promise (axios) wasn’t fulfilled.

However, if the first axios was successful and the second one fails, the error will be printed in error1, not error2. (The second catch method won’t be used at all.) This means that even if you chain multiple Promise calls, you’ll just need only one catch block.

At first, this seemed a little confusing to me, because I thought that each then block should deserve its catch block. (This is the topic of this post, and I’ll explain this in more detail this later)

Multiple async/await


export const multipleAsync = async () => {
  let animals;
  try {
    const result1 = await axios({ method: 'get', url: '/dogs' });
    const result2 = await axios({ method: 'get', url: '/cats' });
    animals = animals.concat(result1.data).concat(result2.data);
  } catch (error1) {
    console.log('error1:', error1);
  }
  return customers;
};

(Once again, you can use Promise.all() here. I wrote this code to discuss today’s topic, which you’ll see soon!)

Like the example of chained Promises above, error1 will have the information of both the 1st and 2nd async calls.

If you’ve worked with JavaScript for a while, it wouldn’t be too difficult to understand both examples.

The problem I’d like to address in this post is…

Problem: how do you handle errors for each Promise (async calls)?

For example, let’s consider the following scenario.

You’re creating a form where the user can create an event. For each event, the user has to determine the number of tickets available.

In this application, you have 2 models, one is Event and the other is Ticket. Each of them has their DB table.

Event
id ❘ name         ❘ address
--------------------------
 1 ❘ ABC Festival ❘ ABC St 1
 2 ❘ XYZ Concert  ❘ XYZ St 1

Ticket
id ❘ event_id
------------
 1 ❘        1
 2 ❘        1
 3 ❘        1

As you can see in this very rudimentary example, one Event can have multiple Tickets.

In order to create this form, you have to make sure that:

  1. A new event is created via a REST API call
  2. Then X number of tickets will be created with the event’s ID via another REST API call
  3. If creating the event fails, you have to show an alert message, saying “Creating the event fail”
  4. If creating the ticket fails, you have to show a different message.

I often find it a little tricky to achieve both 3 and 4, because the chained Promise approach can have only one catch method as illustrated above. (Certainly, Promise.all() can NOT be used because Tickets can be created before their Event is created.) The same goes for the async/await approach, where one catch method gets the errors for all the async calls.

Now, maybe you’re thinking: “How about updating the API calls so that the API for Event and the API for Ticket will produce unique error responses? Then, you’ll need only one catch block!

Of course, it’s not impossible to do so, but I’d like you to forget about the approach now because that’s a different topic.

How do we deal with this type of situation?

// BAD
export const submitEventForm = async ( input ) => {
  try {
    const result = await axios({ 
      method: 'post', url: '/events', data: input.event 
    });
    try {
      await axios({ 
        method: 'post',
        url: '/tickets', 
        data: { event_id: result.data.result.data.id, /* Put the number of tickets here */ }
      });
      return true; // Or, you can return anything else
    } catch (error) {
      window.alert("Failed to create the tickets.");
    }
  } catch (error) {
    window.alert("Failed to create the event.");
  }
};

This is something I used to write. The nested try-catch clauses make the function difficult to read (for me, at least).

How about the following approach?

export const submitEventForm = async input => {
  let event;
  try {
    const result = await axios({ 
      method: 'post', url: '/events', data: input.event 
    });
    event = result.data;
  } catch (error) {
    window.alert("Failed to create the event.");
    // If creating an event fails, this function has to stop here
    return;
  }

  try {
    await axios({ 
      method: 'post',
      url: '/tickets', 
      data: { event_id: result.data.result.data.id, /* Put the number of tickets here */ }
    });
    return true;
  } catch (error) {
    window.alert("Failed to create the tickets.");
  }
};

The second try-catch block was pulled out and put right after the first one. However, I’m not 100% satisfied with this approach yet, because it’s longer than the first approach.

So, I like to abstract API calls to make this type of function shorter.

// READABLE AND SHORTER
export const submitEventForm = async input => {
  let event;
  try {
    event = await createEvent({ input });
  } catch (error) {
    window.alert("Failed to create the event.");
    return;
  }

  try {
    await createTicket({
      event_id: event.id, /* Put the number of tickets here */
    });
    return true;
  } catch (error) {
    window.alert("Failed to create the tickets.");
  }
};

Alternatively, you can create separate functions for each try-catch block, especially if any of those functions can be reused.

Leave a Reply

Your email address will not be published. Required fields are marked *