Many of our API’s have been built as microservices. Recently, I had the task of inheriting one of these microservices, a legacy Node.js codebase. It’s always great working with a service that is brand new to you, but nothing quite prepares you for the feeling that hits you when cloning the git repository, locating the primary app entry point only to be greeted with:
Hopefully not that bad, but we’ve all been there. You want to write a small chunk of code that is going to head off and perform some asynchronous operation while your simple application carries on with it’s day job. Just pop a quick callback in and you might just have add, committed and pushed in time for an early lunch.
Unfortunately you don’t work for an agency, and unmaintainable code stays with you until it’s either A) rewritten, B) scrapped entirely, or worse, C) contributed to by 6 other developers who can’t escape your simple callback that seems to have ballooned into an unreadable, unmaintainable, spaghetti-mess of confused control flow.
We could start writing listeners into our functions, and trigger events as and when we need. This is a fairly clean option, but there is still a potential for listeners being bound and events being triggered in very separate parts of your application. Nice separation of concerns, but if you’ve ever said “Why can’t I find the function that was called when that event was triggered?” you’ll recognise why this is problematic.
More recently, flow control libraries, such as async have made excellent progress in helping us write more readable and maintainable code, but there is still a lot of work to do here in order to handle errors gracefully.
Promises aren’t particularly new. In fact, even jQuery provided introduced promises into their slightly problematic implementation in v1.6 back in 2011. While implementation varies somewhat across languages & libraries, the concept itself is fairly simple…
Every promise starts out in a pending state. From here, it can either become fulfilled (with a return value from your async operation), or rejected (with an error from your async operation).
Let’s say we have an API that exposes a set of async functions that return promises
There is a few problems with the callback style approach that are mitigated with promises:
- Even with just 4 async calls the nesting is quickly getting out of control.
- We have to be very careful about errors, they could be thrown from any of our calls, and we should be prepared to handle them.
- Debugging your application flow becomes very difficult and it can easily confuse what state your application is in at any moment in time
Arguably one of the most difficult aspects of using asynchronous calls in any language is following application flow. As such, Promises allow us to write our application code in a synchronous style, without losing the benefit of async processing. Coupled with clear and concise error handling, Promises allow us to write clean, clear and maintainable code, something you’re likely aiming for if you’ve ever found yourself in callback hell
Not all APIs yet provide a promise implementation (though many do), but luckily there is a handful of great libraries that provide polyfills over existing ‘nodeback’ APIs.
The example above uses
Promise.prototype.then() to chain the promises, ensuring they are fulfilled subsequently giving a more synchronous flow. But what about when we want to handle an API where we know there may be a race condition and as such only want to execute the promise that returns first? We have
Promise.race() for that. Or how about do something when ALL of these promises have resolved?
So next time you’re consuming or writing an API, think about leveraging the power of promises to make your and your fellow developers lives easier.
Senior Software Engineer
There is a swathe of great resources out there on promises, but the following give you most of what you need to make promises work for you.
The open standard for promise implementation
Promise implementations that adhere to the above standard in your favourite languages.