You should never underestimate the complexity of async programming, because it can bite you. The biggest problems are usually mantainability and error handling.
The complexity and unpredictability of Async execution is also one the underlying problems of the pyramid of doom or callback hell problems, but it is an illusion to think that Promises are a silver-bullet.
Promises will not prevent the pyramid of doom
Many people think that Promises are the solution to callback hell, pyramid of doom and all the various async abominations in code, but the thing is that the Promises are not a silver-bullet, and you can easily end up with the same set of problems, here is a promises pyramid of doom (The booking workflow in this code is intentionally simplistic):
I have seen many codebases using code like this (and even worse). The fact is this code is still a pyramid of doom, and in more complex scenarios it can get pretty bad. Main problems in this code:
- The pyramid of doom still exists
- This code is completely unpredictable, since the promises are not executing sequentially
- Crappy code like this leads to many potential bugs
- The error handling code is dispersed in multiple catch functions, and there is a certain degree of duplication in error handling (DRY violation)
You can easily smell a promise pyramid whenever you see
then() blocks going inwards in an existing
The actual solution
The right way to implement async chains is to implement a flat promise chain to ensure sequential promise execution.
The basic principles to follow are:
- Break down execution into distinct small steps that return a promise.
- Keep the top-level promise chain as flat as possible by not nesting
.then()callbacks inwards (try to avoid then() blocks inside then() blocks)
- Have a single
.done()block in the whole top-level promise chain
If you follow these principles you will certainly end-up with much better code.
If we start from the booking code above, the first step would be extracting distinct steps from the processing that can be isolated:
- Save booking
- Send booking confirmation
- Log statistics
- Send HTTP response
We also need to prevent callback nesting, and also stick to one
catch() block to keep error handling in one place.
We can also utilize arrow functions to minimize the verbosity. The resulting code:
This can be further simplified with async-await:
The benefits of the refactoring should be obvious:
- The code is simpler
- There is no inward nesting of then blocks, and each
.then()block or await returns a single promise
- There is a single catch block, which centralizes error handling and resolves the catch/done duplication problem present in promises pyramids.
For more complex scenarios you can also use named functions and keep the code very simple.
Additional reading on the topic of structuring promise-based code: