- In the last article, we discussed javascript Promises and how they solved the problems of callback functions.
- javascript promises are great but chaining them one by one is not very declarative.
- it would be very cool if we are able to write asynchronous code line by line just like normal synchronous code.
- combination of ES6 generators and promise will do just that for us. So let's learn Iterators and Generators.
Iterators:-
let's say you have data source(array or set) and you want to consume values of data source one at a time, for that you can use iterators. you can do that by creating a special object and calling
.next()
over and over on that object until the data source is consumed.now let's look at how we can create the Iterator of an array.
let arr = [1,2,3,4];
let it = arr[Symbol.iterator]()
it.next() // {value : 1, done : false }
it.next() // {value : 2, done : false }
it.next() // {value : 3, done : false }
it.next() // {value : 4, done : false }
it.next() // {value : undefined, done : true }
- you can create the Iterator of data-structure by calling the
Symbol.iterator()
on it. this function call returns an iterator. - On that iterator, you can call the
.next()
method which returns the object with value and done property. - you can call
.next()
untildone
property returnstrue
. - Here, you can see that when the
value
is4
, you don't getdone: true
but you getdone: false
in the next call. because iterator returnsdone: true
only after the data-source is fully consumed. - calling
.next()
over and over is very boring ๐ค. so javascript provide usfor-of
loop which will consume the data-source until the iterator does not returns{ done: false }
const x = [1,2,3,4];
const it = x[Symbol.iterator]();
for(const item of it){
console.log(item);
}
// 1,2,3,4
- with the
for-of
loop you can also use data-structure directly instead of creating an iterator of it but that data-structure should haveSymbol.iterator
method.
const x = [1,2,3,4];
for(const item of x){
console.log(item);
}
- Here,
array
has the defaultSymbol.iterator
method, so you can usefor-of
on the array directly. - In javascript
array
,string
,Map
,Set
,TypedArray
has defaultSymbol.iterator
method. - but not all data-structure has the
Symbol.iterator
method. for example, the main javascript data-typeobject
does not have a default iterator. so In order to leverage the iterator pattern, we need to create our own iterator function. - To create a custom iterator javascript provides an easy and declarative pattern called generator.
- let's let look at the code and I will explain how the generator works.
const obj = {
a:3,
b:4,
c:5,
[Symbol.iterator]:function *() {
const keys = Object.keys(this);
for(let i=0;i<keys.length;i++){
yield keys[i];
}
}
}
const it = obj[Symbol.iterator]();
it.next() // {value : "a", done : false }
it.next() // {value : "b", done : false }
it.next() // {value : "c", done : false }
it.next() // {value : undefined, done : true }
for(const item of obj){
console.log(item) // a,b,c
}
Here, we are providing the function to the
Symbol.iterator
. this is required for creating the iterator but As you can see here we are creating a function withfunction *() {}
syntax. which tells javascript that we are using the generator function.second new syntax is the
yield
keyword. when we call.next()
on iterator created by generator, generator function executes until it encountersyield
, and gives value associated to theyield
to thevalue
property returned bynext()
function.- iterator will return
{done: false}
as long it encountersyield
keyword. after that, it assumes that we have consumed our data-source and it will return the{done: true}
from now on. - good thing about generator is that you can also create a stand-alone generator function. you don't need an
object
to create iterator using generator.
function* makeRangeIterator(start = 0, end = 10) {
for (let i = start; i < end; i ++) {
yield i;
}
}
for(const item of makeRangeIterator()){
console.log(item) // 0,1,2,3,4,5,6,7,8,9
}
- Generators is a very big topic in javascript so if learn more about it you can learn Here. but for now, we have learned everything that is necessary to understand
async/await
.Async/Await :
- for me perfect the async pattern in terms of readability is that it just looks like a normal synchronous but it pauses the function when we are executing async code.
- javascript has run-to-completion semantic. which means that when js engine starts executing a function it will not pause/stop until that function is finished. because of this reason, it was not possible to pause/stop the function.
- But from ES6 we got the generator functions which will be paused when it encounters
yield
and it resumes when we callnext
on the iterator created from the generator function. - let's take a pause and think about what would happen if we combine Promise and generator. i.e what if we can
yield
the response of the promise in the generator function. it may something look like this
perfectAsync(function*(){
const user = yield fetch('/currentuser');
const todos = yield fetch('/todos',{user:user});
console.log(todos);
})
NOTE: this is not the correct javascript code but it is just our imagination.
now let's look at what this code trying to do. imagine we have a function called
perfectAsync
which takes the generator function as an argument and executes the function. whenever weyield
the promise in this function,perfectAsync
will unwrap thefulfillment
response and it will assign to the variable so in this exampleuser
will beobject
, not the promise. and todos will bearray
.inspiring from this pattern ES2017 introduce something called the
async
function. with this function, you can pause the function whenever you are executing something asynchronous and it resumes after executing asynchronous code.
async function getTodosOfUser() {
const user = await fetch('/currentuser');
const todos = await fetch('/todos',{user:user});
console.log(todos); // array of todos.
}
here with help of the
await
keyword whenever we are executing thefetch
request it will pause the function (NOTE: here it will pause the function but not the javascript thread. so your thread will be free to execute asynchronous and synchronous tasks) and whenever we get the response of promise it will unwrap thefulfillment
value for us.error handling async/await is also quite straightforward. you need to use
try/catch
just like synchronous code.
async function getTodosOfUser(){
try{
const user = await fetch('/currentuser');
const todos = await fetch('/todos',{user:user});
console.log(todos); // array of todos.
}catch(e){
console.error(e);
}
}
- async function returns the promise so that you can abstract your code easily.
async getUser(){
const user = await fetch('/currentuser');
return user;
} // returns promise
async function getTodosOfUser(){
try{
const user = await getUser(); // unwraps that promise.
const todos = await fetch('/todos',{user:user});
console.log(todos);
}catch(e){
console.error(e);
}
}
Here,
getUser()
does not return the user object but it returns a promise containing the user object which will be unwarped by theawait
keyword ingetTodoUser
.because of this we always get the expected behavior. (if the async function would return the simple value,await
keyword will not able to unwrap the response because it always expects the promise. because of this it always returns a promise).As you can
Async/await
is just syntactic sugar aroundpromise
. so In your codebase, if you use promises,async/await
will just fit in fine.that's it, folks. I think now you know how
async/await
works under the hood in javascript and how they are just syntactic sugar around promise and generator; not some alien tech.