Today, I am writing a TIL post on asynchronous programming. It’s something that I’ve learned last year and have been reviewing repeatedly… And yet, I would always say I only vaguely knew it. This time around, with the help of Eunjae’s Sinabro JavaScript course as well as a Korean book on how to become a Node.js backend developer, there were definitely moments where I got a better grasp of an understanding.
To solidify my takeaways, this TIL is on asynchronous programming such as callbacks, Promises, and async/await.
JavaScript is single-threaded - it can only work on one thing at a time
-
However, JavaScript often deals with requests that take an extended amount of time -> this means that as a single-threaded language, if a request takes 10s to complete, the rest of the code will have to wait prior to being run.
-
Call stack: main thread of execution that is often likened to a pancake stack. Whenever a function is called, it is added to the top of the stack. The function on the top is executed first - once it finishes executing it is removed from the call stack.
-
Web APIs: Often, JavaScript can delegate certain asynchronous tasks such as setTimeout and fetch to the Browser via web APIs. JavaScript tosses it to the Web APIs and lets them handle it and continues to execute the next line of code.
-
Callback queue: When a Web API operation or asynchronous operation is complete, a callback function is added to the callback queue.
-
If and only when the call stack is empty, does the event loop add the callback function and push it to the call stack.
-
Within a Promise object,
resolve
andreject
are essentially values or callback functions you want to run if the promise reaches resolve or oreject status.
Asynchronous vs. Synchronous:
Synchronous:
- Operations that are executed one after another in a sequential order
- Each operation finishes before the next one starts
Asynchronous:
- Operations can start and complete independently of the main program’s flow
- Not necessarily run in a set sequential order
If you have an asynchronous operation on TOP of a console.log(“hello”), because it takes time for the asynchronous operation to run, the asynchronous function is pushed to the side of the main flow and the next line of code runs. Therefore, with asynchronous operations, the results of codes don’t necessarily show in the console in a top-down fashion. If you wanted the console.log to run AFTER the asynchronous operation is done, you would pass it into the callback function.
However, if you have a series of steps that should follow an asynchronous operation (rather than run before the asynchronous function is complete), using callbacks would result in callback hell (numerous number of indentations that each denotes a callback).
Important Note: console.log(Promise)
and Promise.then(console.log)
is different
- Promise is essentially a ‘promise’ of a future value that is available once the asynchronous operation is complete.
- By chaining the promise with a
.then
and passing console.log inside it, you are essentially saying ONCE the promise is fulfilled (either by resolving or reject), execute this callback function. - Since we’re usually interested the end result value after a Promise has ended, you would therefore chain it with a then and console.log the resulting value.
- If you have several Promises that you want to take place in a certain order, use
then
and chain (e.g. if the successful resolution of promise2 depends on a value returned by promise1, then you should be chaining promise1.then(promise2).
Promise.all()
If your multiple promises don’t necessarily need to be run in an order, and/or a promise doesn’t rely or depend on the result of another promise, but you’d like to see their results, you can use Promise.all()
.
One thing to note about Promise.all()
is that if any of the Promises in the array is rejected, the Promise.all() also is rejected. If you would like to see the rest of the results of the promises even if one of them is rejected, you would use Promise.allSettled()
.
Async & await
- Even though it is not stated explicitly in the code, an async function returns a promise.
- Await keyword essentially is denoting that you’d like to wait until the promise is no longer pending and to receive the return value. (Aka, I want the rest of the code to wait!)
- Look at the following code:
// fetch request
const promise = fetch(url)
// not using await keyword
const response1 = promise;
console.log({response1}) // you get a Promise value
// using await keyword
const response2 = await promise;
console.log({response2}) // you get the actual returned value of a fulfilled Promise
Thing to note about an async
function that has a return value with await
keyword
Let’s take a look at the code below (code is provided by Eunjae from his course).
// first example without async/await keyword
function getTodo(id) {
return fetch(url)
}
console.log('getTodo(1)', getTodo(1));
The result of the console.log would be a Promise, since fetch returns a Promise. This means that you wouldn’t be receiving a final value of a fulfilled Promise.
Let’s now try to recreate this function into an asynchronous function using await
and async
.
async function getTodoAsync(id) {
return await fetch(url)
}
console.log('getTodoAsync(1)', getTodoAsync(1))
You may think that the log of this function would return an actual value since await
is used. Surprisingly, it still returns a Promise.
This is because the function is declared as an async
function. An async
function will always return a promise, no matter what it contains. The return value has await
, which means the function would pause its execution until the promise returned by fetch(url)
is resolved. Even so, the getTodoAsync function will still return a promise.
Examples of methods or functions that return promises
.json()
fetch()
- requests to network
- file reader