How to handle Promise errors gracefully

Keywords: Javascript Go

Pay attention to the front of the public, Chinese cabbage, more front-end small dry goods waiting for you! The public number will share the front-end technology from time to time, making progress every day and growing with everyone.

With the emergence of Promise, the majority of front-end developers have been successfully saved from the abyss of callback region, and then there is Async/Await, which makes it easier for developers to write asynchronous code, and when dealing with errors, it seems that it is not so beautiful.

First simulate a request operation:

const fetchData = (url, shouldReject = false) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      shouldReject ? reject('error') : resolve('this is data');
    }, 1000);
  });
};

Briefly review Promise and Async/Await. For example, three requests can be initiated by using Promise

fetchData('url1')
  .then(data => {
    console.log('data1: ', data);
    return fetchData('url2');
  })
  .then(data => {
    console.log('data2: ', data);
    return fetchData('url3');
  })
  .then(data => {
    console.log('data3: ', data);
  })
  .catch(error => console.log('error: ', error));

It looks good, which is the advantage of Promise over its callback function. The code looks elegant, but Async/Await can be more elegant.

(async () => {
  const data1 = await fetchData('url1');
  console.log('data1: ', data1);
​
​
  const data2 = await fetchData('url2');
  console.log('data2: ', data2);
​
​
  const data3 = await fetchData('url3');
  console.log('data3: ', data3);
})();

The code looks beautiful. Even Async/Await is more like synchronous code and more in line with human's normal logic. But look at the Promise example again. All exceptions are handled at the end. When an exception occurs, which one is requested? Is it necessary to define the wrong type?

Of course, Promise can catch exceptions one by one,

fetchData('url1')
  .catch(error => console.log('error1: ', error))
  .then(data => {
    console.log('data1: ', data);
    return fetchData('url2', true);
  })
  .catch(error => console.log('error2: ', error))
  .then(data => {
    console.log('data2: ', data);
    return fetchData('url3');
  })
  .catch(error => console.log('error3: ', error))
  .then(data => {
    console.log('data3: ', data);
  });
// output
// data1: this is data
// error2: error
// data2: undefined
// data3: this is data

It can be seen that although every exception can be caught, the exception processing has been done at every layer, so that project will not terminate execution due to reject, and it is also very troublesome to write, which is not what we want.

Async/Await is the syntax sugar of Promise. We can use try catch to catch exceptions or Promise catch to catch exceptions.

(async () => {
  try {
    const data1 = await fetchData('url1', true);
    console.log('data1: ', data1);
  } catch (error) {
    console.log('error1:', error);
  }
​
  const data2 = await fetchData('url2', true).catch(error => console.log('error2: ', error));
  console.log('data2: ', data2);
​
  const data3 = await fetchData('url3').catch(error => console.log('error3: ', error));
  console.log('data3: ', data3);
})();
​
// output
// error1: error
// error2: error
// data2: undefined
// data3: this is data

If you use Promise's catch to catch exceptions and handle the return value, you can implement exception handling well and prevent further execution in case of errors.

(async () => {
  const data1 = await fetchData('url1', true).catch(error => console.log('error1: ', error));
  if (!data1) return;
  console.log('data1: ', data1);
​
  const data2 = await fetchData('url2', true).catch(error => console.log('error2: ', error));
  if (!data2) return;
  console.log('data2: ', data2);
​
  const data3 = await fetchData('url3').catch(error => console.log('error3: ', error));
  if (!data3) return;
  console.log('data3: ', data3);
})();
// output
// error1: error

Is there a more elegant way of writing? Of course, if you do a little processing for the business functions using Promise and combine Promise with Async/Await, you can handle asynchronous business more elegantly.

// Normal unhandled asynchronous function
const fetchDataPromise = (url, shouldReject = false) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      shouldReject ? reject('error') : resolve('this is data');
    }, 1000);
  });
};
​
// General asynchronous function processing
const fetchData = (url, shouldReject = false) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      shouldReject ? resolve(['error', null]) : resolve([null, 'this is data']);
    }, 1000);
  });
};
// Handling asynchronous functions
const asyncFunc = (url, shouldReject = false) => {
  return fetchDataPromise(url, shouldReject)
    .then(data => [null, data])
    .catch(error => [error, null])
}
​
// Extracting common functions to deal with asynchronous functions
const promiseWrap = (promise) => {
  return promise
    .then(data => [null, data])
    .catch(error => [error, null])
}

With the above modifications, the business code can be written in this way.

(async () => {
  const [error1, data1] = await fetchData('url1', true);
  if (error1) {
    console.log('error1: ', error1);
    // do more thing
  }
  console.log('data1: ', data1);
​
  const [error2, data2] = await asyncFunc('url2', true);
  if (error2) {
    console.log('error2: ', error2);
  }
  console.log('data2: ', data2);
​
  const [error3, data3] = await promiseWrap(fetchDataPromise('url3', true));
  if (error3) {
    console.log('error3: ', error3);
  }
})();
// output
// error1: error
// data1: null
// error2: error
// data2: null
// error3: error
// data3: null

In this way, it is more elegant and easier to handle exceptions. In fact, this processing method is very common in GO language.

Posted by shaitand on Tue, 12 Nov 2019 02:14:23 -0800