Promises are essential to Javascript and a fundamental tool to handle asynchronous calls. However, promises are often used in the wrong way and produce poorly readable code. In this blog entry, I want to show how you can better combine dependent promises.
Promises have been introduced in the Javascript standard with ES6. Before that, there were several libraries like JQuery or AngularJS which had their own implementations. With promises you guarantee that something will happen later (a nice metaphor).
In the past, callback functions were often used which are subsequently called in some part of the program. Promises define that in the future a defined logic will be executed. In the event of an error another logic can and should be executed. A great benefit is that you can also define dependencies between such promises – but unfortunately they are often programmed more clunky than necessary.
As some colleagues know, I am a passionate spaghetti Bolognese chef and eater. Therefore it is only natural for me to explain the concept based on Italian cuisine. Let’s literally write a little spaghetti code!
First however, until I have my food on the table, there is some work to be done first. The spaghetti sauce and the pasta have to be cooked. So in this example I have the functions “cookSpaghetti” and “cookBolognese”.
The code could look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var cookSpaghetti = (minutes) => { return new Promise(function (resolve, reject) { if (minutes < 10) { reject({ state: 'raw' }); } else if (minutes > 12) { reject({ state: 'overcooked' }); } setTimeout(() => { resolve({ state: 'al dente' }) }, minutes); }); } var cookBolognese = () => { return new Promise(function (resolve, reject) { // ... we will see that later setTimeout(() => { resolve({ state: 'finished' }) }, 100); }); } |
Our spaghetti should be cooked between 10 and 12 minutes, otherwise they will be either over- or undercooked. The “cookBolognese” function is designed on an abstract level and finishes after 100ms. To shorten our time of waiting, the minutes are depicted as milliseconds. 10 actual minutes while cooking translate to 100ms waiting in the code.
The first coding attempt could look something like this:
1 2 3 4 5 6 7 8 9 10 11 |
cookSpaghetti(10).then(res => { console.log('state of my Spaghetti: ' + res.state); cookBolognese().then(res => { console.log('state of my Bolognese: ' + res.state); console.log('Meal is finished'); }, err => { console.log('Cooking spaghetti did work but my bolognese is not eatable. I give up and order something from the delivery service.'); }); }, err => { console.log('Cooking spaghetti did not work. I give up and order something from the delivery service.'); }); |
I cook the spaghetti for 10 minutes and then start with the Bolognese. If the spaghetti were cooked less than 10 or more than 12 minutes my dream of the perfect Italian pasta would be destroyed and I would have to order a Pizza from the delivery service instead.
As you can see, “cookSpaghetti” is executed first and then “cookBolognese”. But since I am an experienced and efficient chef I can use two pots simultaneously. The Bolognese does not have to wait 10 minutes for the spaghetti to finish. Dinner is ready when both tasks are completed – so since they do not rely on each other, both can start at the same time. With that in mind, here is the improved version:
1 2 3 4 5 |
var p1 = cookSpaghetti(10); var p2 = cookBolognese(); Promise.all([p1, p2]) .then(values => console.log('Meal is finished')) .catch(err => console.log('Something went wrong. I give up and order something from the delivery service.')); |
In this example both “cookSpaghetti” and “cookBolognese” are executed at the same time and we wait for the results of both functions. If something goes wrong I will again order something from the delivery service. This way the code looks a lot cleaner and is easier to read. If I now manage to stick to the 10 minutes cooking time nothing can go wrong.
Up until now everything looks pretty easy. But keep this question in mind: Is a more efficient simultaneous execution of my code possible or is a sequential execution necessary?
Cooking spaghetti is not a hard thing to do. Put them in boiling water and let them cook for the specified amount of time. So let’s take a closer look at the “cookBolognese” function instead. Two onions have to be cut, the meat needs to be roasted, the garlic pressed and the tomato sauce cans opened. Subsequently everything gets mixed together and flavored. Those are a lot of steps to be executed in a sequence to make the perfect sauce. So, first I want to cut the onions and cook them, afterwards the meat has to be roasted, then we need to add the garlic, flavor everything and then at the end add the tomato sauce and let the whole thing cook slowly.
For lack of space we keep this next version of the code simple:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
var cutAndSearOnions = (ingredients) => { return new Promise(function (resolve, reject) { ingredients.push('onion') setTimeout(() => { resolve(ingredients) }, 100); }); } var searMeat = (ingredients) => { return new Promise(function (resolve, reject) { ingredients.push('meat') setTimeout(() => { resolve(ingredients) }, 100); }); } var pressGarlic = (ingredients) => { return new Promise(function (resolve, reject) { ingredients.push('garlic') setTimeout(() => { resolve(ingredients) }, 100); }); } var addSpices = (ingredients) => { return new Promise(function (resolve, reject) { ingredients.push('spices') setTimeout(() => { resolve(ingredients) }, 100); }); } var addTomatoes = (ingredients) => { return new Promise(function (resolve, reject) { ingredients.push('tomatoes') setTimeout(() => { resolve(ingredients) }, 100); }); } |
Every function adds something to the list of ingredients.
The “cook” function then evaluates if all the necessary ingredients exist and displays whether cooking our meal was successful or not.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var cook = (ingredients) => { var neededIngredients = ['onion', 'meat', 'garlic', 'spices', 'tomatoes']; return new Promise(function (resolve, reject) { if (ingredients != null) { neededIngredients.forEach(neededIngredient => { if (!ingredients.includes(neededIngredient)) { reject('Badly cooked. Something is missing: ' + neededIngredient); } }); resolve('Meal is finished'); } else { reject('There are no ingredients!'); } }); } |
Often the code would look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
cutAndSearOnions([]).then(ingredients => { searMeat(ingredients).then(ingredients => { pressGarlic(ingredients).then(ingredients => { addSpices(ingredients).then(ingredients => { addTomatoes(ingredients).then(ingredients => { cook(ingredients).then(ergebnis => { console.log(ergebnis); }, err => { console.log('Something went wrong with the meal. I give up and order something from the delivery service.'); }); }, error => { console.log('Error'); }); }, error => { console.log('Error'); }); }, error => { console.log('Error'); }); }, error => { console.log('Error'); }); }, error => { console.log('Error'); }); |
That whole thing is hardly readable. One thing nested in another nested in another nested in yet another and so on. With every line the code dives deeper and deeper and in case of an error it gets unnecessarily complicated. That’s exactly what happens when you are not prepared when cooking and the kitchen afterwards is a disaster. Dinner is ready but the kitchen is left in a catastrophic state and now you can’t find anything in that mess anymore. A more elegant, shorter and more readable solution is the following one:
1 2 3 4 5 6 7 8 9 10 |
cuttingAndSearingOnions([]) .then(searMeat) .then(pressGarlic) .then(addSpices) .then(addTomatoes) .then(cook) .then(result => console.log(result)) .catch(err => { console.log('Something went wrong with the meal. I give up and order something from the delivery service.'); }); |
The code is readable and at one glance it is understandable what’s happening. Additionally it is easy to see what happens in the case of an error without having to follow deeply nested Promises. All the errors get taken care of at the end in one single catch block.
Why do people decide to use version 1 regardless? I think the reason is that if you generally know how to write a Promise you will write the second Promise in the same way.
If you know though that it’s sufficient to pass the function as first parameter of “then” and that the result will be passed along then everything becomes clearer and the code will be better structured.
Promises are an elegant tool if used correctly. The example shows how you can arrange things in a more structured manner. Like the kitchen the code should be kept clean. Bon appetite!
English Translation: Maximilian Baumann