Basics of fault-tolerant Promises in TypeScript

Recently I’ve been spending some time developing an Ionic 2 mobile app, which uses Angular and TypeScript.

I have noticed a couple places where my team has code similar to this:

someCallThatReturnsAPromise().then( () => {
  someOtherCallThatReturnsAPromise().then( () => {
     someThirdPromiseCall();
  });
}, (err) => {
  this.alertHelper.triggerErrorAlert("Hi an error happened: " + err.message);
});

This works but it suppresses errors. This would only execute the err function if the first call (someCallThatReturnsAPromise()) errors out. Not the other two.

There is a better way! If you write code like this, you may notice a warning from the IDE or your linter saying that you are ignoring the result of a promise. Don’t ignore that helpful message! If you want the app to be stable and fault-tolerant, it is just as easy to chain the promises together correctly instead of nesting them. And you get the benefit of fault-tolerance this way!

The above example can be rewritten as follows:

someCallThatReturnsAPromise().then( () => {
  return someOtherCallThatReturnsAPromise();
}).then( () => {
  return someThirdPromiseCall();
}).catch( (err) => {
  this.alertHelper.triggerErrorAlert("Hi an error happened: " + err.message);
});

What this does is it passes the result of each promise along to the next piece in the chain. That includes any errors. The .catch at the end will catch an error arising in any one of the three calls.

You can use this to your advantage to stop the whole chain and throw an error instead if some condition is not satisfied for continuing. Consider this:

return fetchUserUid().then( (uid) => {
  if(!uid) {
    return Promise.reject("Unable to fetch user right now. Try again later!");
  }
  return fetchProfileForUser(uid);
}).then( (profile) => {
  if(!profile) {
    return Promise.reject("User profile unavailable. Try again later!");
  }
  return profile.fetchUserName();
}).catch( (error) => {
  this.alertHelper.triggerErrorAlert(err.message);
});

In this example, if the call to fetchUserUid fails none of the rest of it executes because the promise is rejected and the catch block immediately executes, which will put an error alert up with the message “Unable to fetch user right now. Try again later!” If the call to fetchUserUid succeeds, but the call to fetchProfileForUser fails, the catch block will execute and put up the message “User profile unavailable. Try again later!”

If both succeed, then the third call to profile.fetchUserAddress() will get called.

In other words, it is called a “promise chain” because it is a chain of events that you have created, but each link in the chain will only execute if the previous one has resolved. That brings up another useful static method: Promise.resolve(). Like Promise.reject() you can use it to easily return a value. It is analogous to the following:

return new Promise((resolve: Function) => {
      resolve();
    });

An example of how you might use this is to gracefully tolerate faults by supplying a default value. Let’s say all you want to do with the previous code is display a greeting at the top of the page. Then you probably don’t need to throw up an error alert on the error case. You can simply put “Hello and welcome!” instead:

return fetchUserUid().then( (uid) => {
  if(!uid) {
    return Promise.resolve(undefined);
  }
  return fetchProfileForUser(uid);
}).then( (profile) => {
  if(!profile) {
    return Promise.resolve("Hello and welcome!");
  }
  return profile.fetchUserName();
}).catch( (error) => {
  this.alertHelper.triggerErrorAlert(err.message);
});

Now the catch block will still execute if an error is thrown but where we used Promise.reject before we now actually pass along a value via Promise.resolve which ultimately will resolve to “Hello and welcome!” for the case where either fetchUserUid() returns undefined or fetchProfileForUser returns undefined.

Hope it helps!

Leave a Reply

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