Error handling in long Promise chains
The longer the chain, the easier it is to break it
So you’re writing async code in JavaScript, right? I assume you are using Promises — if not, well… you should be. Chances are as well that you’re using a microservice-oriented approach to your application. These settings can bring some problems to your application regarding error handling, especially if you make use of some complex business logic to access those microservices.
In my case I was developing a giant form for a web application using AngularJS. Every section of this form would then, on submit, be translated to an API call onto different microservices and endpoints. As a dedicated developer, I’ve started working on this form, and thus began the adventure.
The tale of one giant form
Once upon a time, there was a giant form. It called different API endpoints on submit. Simple, efficient and pretty, although naive; this was the code for the submit function of that form:
saveApplication()
.then(uploadImages)
.then(saveService)
.then(savePricingInfo)
.then(savePaymentInfo)
.then(gotoMainPage)
.catch(setErrorState);
Where all the functions on the Promise chain such as uploadImage, saveService, etc. are functions that return a Promise object from an API call (using Angular’s ngResource).
But what we didn’t knew was that those innocent days were about to change. Darkness was silently spreading its roots below our kingdom. We started to notice it only when we’ve decided to change this giant form into a wizard-like multi-step form. This introduced a small problem which could have been easily solved at the beginning had we had the right approach in mind. But we didn’t. We did opt for that wooden sword to fight a medium-sized monster. Which worked, but was not good enough.
Battling the medium-sized monster with a wooden sword
It turns out every step of the form, that is, every API call on submit could return a different error, which should be mapped to a specific field and step of the multi-step form. We had to have a way of identifying which step corresponded to the error when it happened.
Our multi-step form had four steps then, namely: basic, display, pricing and payment. Our first (and childish) approach was, at first, to add a .catch block after every .then which contained an API call. This catch would then have, hardcoded, the corresponding step for redirection.
This is what it looked like:
saveApplication()
.catch(handleError(’basic’))
.then(uploadImages)
.catch(handleError(’display’))
.then(saveService)
.catch(handleError(’display’))
.then(savePricingInfo)
.catch(handleError(’pricing’))
.then(savePaymentInfo)
.catch(handleError(’payment’))
.then(gotoMainPage);function handleError(step) {
return (err) => {
if(err.break) return $q.reject(err);
setErrorState();
multiStepManager.go(step);
err.break = true;
return $q.reject(err);
};
}
Whoa! No need for all that, Joe. At the end, this was like using a cannon to kill a fly. But it worked. But it was a total mess. But it worked…
The handleError function returned a function which handled the error for that specific step on the Promise chain. It then would set a property (break) on the error object — which gets passed down along the chain to .catch blocks — , what would prevent other .catch blocks to handle that same error.
It turns out that, in the end, the medium-sized monster was only a fly and the wooden sword was actually a huge badly engineered cannon.
The Tao of Promises
With a little more experience and understanding of Promises and the monadic laws that govern its use, we wouldn’t have spent so much time working on the refactor of this piece of code.
In JavaScript, asynchronous code can be handled via Promises, which are the Continuation monads of JavaScript. The most important thing in this case about Promises being a monad is that the following law is true:
Promise.resolve(Promise.resolve(x)) === Promise.resolve(x).
And more important than the one above, this one also holds for any value x:
Promise.resolve(Promise.reject(x)) === Promise.reject(Promise.resolve(x)) === Promise.reject(x).
This magic rule above is what saved me some thought power at the end of the day. This meant I could treat those errors as soon as they happened, inside their own functions, staying away from that long chain madness. The answer was always there staring at me, I just couldn’t see it. Now I see, and it’s beautiful.
This meant I could simply have the saveApplication function like this, for example:
function saveApplication() {
return makeApiCall().catch((err) => Promise.reject('basic'));
}
The .catch block means that we are handling an error on the basic step of the form, because the saveApplication call is related to the step of the form called basic. This led us to the beautiful piece of code down below:
saveApplication()
.then(uploadImages)
.then(saveService)
.then(savePricingInfo)
.then(savePaymentInfo)
.then(gotoMainPage)
.catch((step) => {
setErrorState();
multiStepManager.go(step);
});
We only had to change a single line of the Promise chain, now that all the functions inside the .then blocks returns a Promise which already rejects to the corresponding step.
But what if other types of errors happened, which were not handled by the inner catches?
Well, that could be easily solved by implementing custom error types, and separating the evaluation of different error types inside our main .catch block. Like this:
function saveApplication() {
return makeApiCall().catch((err) => Promise.reject(new StepError('basic')));
}//
saveApplication()
.then(uploadImages)
.then(saveService)
.then(savePricingInfo)
.then(savePaymentInfo)
.then(gotoMainPage)
.catch((step) => {
if(err instanceof StepError) {
setErrorState();
multiStepManager.go(step);
}
else {
// handle other types of errors
}
});
In this case, the main .catch block only handles errors of type StepError. Other types of errors are simply thrown, not rejected, so that they can be handled accordingly by the application or the browser.
The same principle can and should be extended to handle specific error types, such as different HTTP statuses.
The end
Working with long Promise chains can easily and quickly become one hell of a mess if you don’t stick to the rules. It’s actually really easy to achieve a good structure using Promise chains. Building the right mindset is the laborious part.
All in all, here’s what I’ve learned from this quest:
- If you don’t have only one single .catch block in your Promise chain, you’re doing it wrong. Sorry to say it like this, but it’s true;
- Custom error types on JavaScript can be cool;
- It’s important to really know the rules of the tools you’re using. In this case, Promises.
If you liked these rules and this exciting quest, please hit the green heart button below and make me happy! :D