Anyone who considers himself a JavaScript developer, at some point, should have encountered callback functions, promises, or, more recently, the async / await syntax. If you've been in the game long enough, you've probably come across times when nested callback functions were the only way to achieve asynchrony in JavaScript.
When I started learning and writing in JavaScript, there were already a billion tutorials and tutorials explaining how to achieve asynchrony in JavaScript. However, many of them simply explained how to convert callback functions to promises or promises in async / await. For many, this is probably more than enough to get along with them and start using them in their code.
However, if you, like me, really want to understand asynchronous programming (and not just JavaScript syntax!), Then you may agree with me that there is a shortage of material explaining asynchronous programming from scratch.
What does asynchronous mean?
As a rule, asking this question, you can hear something from the following:
- There are several threads that execute code at the same time.
- More than one piece of code is executed at a time.
- This is concurrency.
To some extent, all options are correct. But instead of giving you a technical definition that you are likely to forget soon, I will give an example that even a child can understand .
Life example
Imagine you are cooking vegetable soup. For a good and simple analogy, suppose a vegetable soup consists only of onions and carrots. The recipe for such a soup may be as follows:
- Chop the carrots.
- Chop the onion.
- Add water to the pan, turn on the stove and wait until it boils.
- Add the carrots to the pan and leave for 5 minutes.
- Add the onions to the pan and cook for another 10 minutes.
These instructions are simple and understandable, but if one of you, reading this, really knows how to cook, you can say that this is not the most effective way of cooking. And you will be right, that’s why:
- Steps 3, 4 and 5 do not actually require you as a chef to do anything except to observe the process and keep track of time.
- Steps 1 and 2 require you to actively do something.
Therefore, the recipe for a more experienced cook may be as follows:
- Start boiling a pot of water.
- While waiting for the pan to boil, start cutting carrots.
- By the time you finish chopping the carrots, the water should boil, so add the carrots.
- While carrots are cooked in a pan, chop the onions.
- Add onions and cook another 10 minutes.
Despite the fact that all actions remained the same, you have the right to expect that this option will be much faster and more efficient. This is precisely the principle of asynchronous programming: you never want to sit back, just waiting for something, while you could spend your time on some other useful things.
We all know that in programming, waiting for something happens quite often - whether it is waiting for an HTTP response from a server or an action from a user or something else. But the execution cycles of your processor are precious and should always be used actively, doing something, and not expecting: this gives you asynchronous programming .
Now let's get to JavaScript, okay?
So, following the same example of vegetable soup, I will write a few functions to represent the steps of the recipe described above.
First, let's write synchronous functions that represent tasks that don't take time. These are the good old JavaScript functions, but note that I described the
chopCarrots
and
chopOnions
as tasks that require active work (and time), allowing them to do some long calculations. Full code is available at the end of the article [1].
function chopCarrots() { /* ... */ console.log(" !"); } function chopOnions() { /* ... */ console.log(" !"); } function addOnions() { console.log(" !"); } function addCarrots() { console.log(" !"); }
Before moving on to asynchronous functions, I will first quickly explain how the JavaScript type system handles asynchrony: basically all the results (including errors) of asynchronous operations should be wrapped in a promise (s) .
For a function to return a promise, you can:
- explicitly return the promise, i.e.
return new Promise(…)
; - implicitly return a promise - add the
async
to the function declaration, i.e.async function foo()
; - use both options .
There is an excellent article [2], which talks about the difference between asynchronous functions and functions that return a promise. Therefore, in my article I will not dwell on this topic in detail, remember the main thing: you should always use the
async
in asynchronous functions.
So, our asynchronous functions, representing steps 3-5 of preparing vegetable soup, are as follows:
async function letPotKeepBoiling(time) { return; // , } async function boilPot() { return; // , }
Once again, I deleted the implementation details so as not to be distracted by them, but they are published at the end of the article [1].
It is important to know that in order to wait for the result of the promise, so that later you can do something with it, you can simply use the
await
keyword:
async function asyncFunction() { /* ... */ } result = await asyncFunction();
So, now we just need to put it all together:
function makeSoup() { const pot = boilPot(); chopCarrots(); chopOnions(); await pot; addCarrots(); await letPotKeepBoiling(5); addOnions(); await letPotKeepBoiling(10); console.log(" !"); } makeSoup();
But wait! This does not work! You will see the
SyntaxError: await is only valid in async functions
error. Why? Because if you do not declare a function using the
async
, then by default JavaScript defines it as a synchronous function - and synchronous means no waiting! [3]. It also means that you cannot use
await
outside of a function.
Therefore, we simply add the
async
to the
makeSoup
function:
async function makeSoup() { const pot = boilPot(); chopCarrots(); chopOnions(); await pot; addCarrots(); await letPotKeepBoiling(5); addOnions(); await letPotKeepBoiling(10); console.log(" !"); } makeSoup();
And voila! Please note that in the second line, I call the asynchronous function
boilPot
without the
await
keyword, because we do not want to wait for the pan to boil before we start cutting the carrots. We only expect the promise in the fifth line before we need to put the carrots in the pan, because we do not want to do this before the water boils.
What happens during
await
calls? Well, nothing ... sort of ...
In the context of the
makeSoup
function
makeSoup
you can simply think of it as that you expect something to happen (or a result that will eventually be returned).
But remember: you (like your processor) will never want to just sit there and wait for something, while you can spend your time on other things .
Therefore, instead of just cooking soup, we could cook something else in parallel:
makeSoup(); makePasta();
While we are waiting for
letPotKeepBoiling
, we can, for example, cook pasta.
See? The async / await syntax is actually pretty easy to use, if you understand it, agree?
What about overt promises?
Well, if you insist, I will turn to the use of explicit promises ( approx. Transl .: by explicit promises, the author implies directly the syntax of the promises, and by implicit promises the syntax async / await, because it returns the promise implicitly - no need to write
return new Promise(…)
). Keep in mind that the async / await methods are based on the promises themselves and therefore both options are fully compatible .
Explicit promises, in my opinion, are between the old-style callbacks and the new async / await sexual syntax. Alternatively, you can also think of the async / await sexual syntax as nothing more than implicit promises. In the end, the async / await construct came after the promises, which in turn came after the callback functions.
Use our time machine to move to the callback hell [4]:
function callbackHell() { boilPot( () => { addCarrots(); letPotKeepBoiling(() => { addOnions(); letPotKeepBoiling(() => { console.log(" !"); }, 1000); }, 5000); }, 5000, chopCarrots(), chopOnions() ); }
I'm not going to lie, I wrote this example on the fly when I was working on this article, and it took me a lot more time than I would like to admit. Many of you may not even know what is going on here. My dear friend, aren't all these callback functions awful? Let it be a lesson to never use callback functions again ...
And, as promised, the same example with explicit promises:
function makeSoup() { return Promise.all([ new Promise((reject, resolve) => { chopCarrots(); chopOnions(); resolve(); }), boilPot() ]) .then(() => { addCarrots(); return letPotKeepBoiling(5); }) .then(() => { addOnions(); return letPotKeepBoiling(10); }) .then(() => { console.log(" !"); }); }
As you can see, promises are still similar to callback functions.
I will not go into details, but most importantly:
-
.then
is a promis method that takes its result and passes it to the argument function (essentially, to a callback function ...) - You can never use the result of a promise outside the context of
.then
. Essentially, .then is like an asynchronous block that expects a result and then passes it to a callback function. - In addition to the
.then
method, there is another method in.catch
-.catch
. It is needed to handle errors in promises. But I will not go into details, because there are already a billion articles and tutorials on this topic.
Conclusion
I hope you got some idea about promises and asynchronous programming from this article, or perhaps at least learned a good example from life to explain this to someone else.
So, which way do you use: promises or async / await?
The answer is completely up to you - and I would say that combining them is not so bad, since both approaches are completely compatible with each other.
Nevertheless, I personally am 100% in the async / await camp, since for me the code is much more clear and better reflects the true multitasking of asynchronous programming.
[1] : The full source code is available here .
[2] : Original article “Async function vs. a function that returns a Promise " , translation of the article " The difference between an asynchronous function and a function that returns a promise . "
[3] : You can argue that JavaScript can probably determine the async / await type from the body of functions and recursively check, but JavaScript was not designed to take care of static type safety at compile time, not to mention that it’s much more convenient for developers to explicitly see the type of function.
[4] : I wrote “asynchronous” functions, assuming that they work under the same interface as
setTimeout
. Note that callbacks are not compatible with promises and vice versa.