Heads Up
- I am assuming you know basic JavaScript (>= ES6)
- This post is rather semantic than syntactic
- I have put up links for further explanations wherever I thought would be necessary.
Let's Go
Examples are the best way to make someone understand something because it's easier to correlate with something practical than digest theory - Asif Mohammad
For example, every time we search for the meaning of a word online we tend to read its examples to make more sense.
See what I did there? 😉
So lets just consider an example of baking and eating some delicious cake. We can break down the whole process into three basic steps
- Baking Cake
- Serving Cake
- Eating Cake
The Javascript equivalent of this process could be portrayed as the following functions
const bakeCake = () => console.log('Cake is baked');
const serveCake = () => console.log('Cake is served');
const eatCake = () => console.log('Cake eaten');
Yes, it is neither a proper equivalence nor I am eligible to be a good cook but it serves the purpose if not the cake.😉
Our cake baking journey would go something like
bakeCake(); // Cake is baked
serveCake(); // Cake is served
eatCake(); // Cake is eaten
But most real world scenarios like baking cake and scenarios on the web like fetching user posts, have something in common, they take time
Lets adjust our functions so that they reflect such and lets consider each of our step takes 2 seconds of time
const bakeCake = () => {
setTimeout(()=>{
console.log('Cake is baked')
}, 2000);
};
const serveCake = () => {
setTimeout(()=>{
console.log('Cake is served')
}, 2000);
};
const eatCake = () => {
setTimeout(()=>{
console.log('Cake is eaten')
}, 2000);
};
We cannot call these three functions sequentially because they will not run synchronously. Why?
So we should follow the standard callback pattern which is being used for a long time now.
Using Callback Functions
const bakeCake = (cbkFn) => {
setTimeout(()=>{
console.log('Cake is baked');
cbkFn();
}, 2000);
};
const serveCake = (cbkFn) => {
setTimeout(()=>{
console.log('Cake is served');
cbkFn();
}, 2000);
};
const eatCake = () => {
setTimeout(()=>{
console.log('Cake is eaten')
}, 2000);
};
bakeCake(()=>{
serveCake(()=>{
eatCake();
});
});
Understanding the callback pattern
When we use callbacks we expect the function we pass to be called back when required (hence the name callback functions). The problem with callbacks is the often occurring Callback Hell.
Consider our cake baking, when the steps are extended it becomes
bakeCake(() => {
decorateCake(() => {
tasteCake(() => {
cutCake(() => {
serveCake(() => {
eatCake(() => {
});
});
});
});
});
});
This is what we call as the Callback Hell. The more things you are willing to do in this process the more complex and messy it will get. It works, It's fine but we always want something batter better.
Promise
Promise as the name goes is a pattern, rather than being an object/function, where you are promised the execution of a piece of code and it enables you to code further based on your trust on that promise. JS Engine is a machine so you can always trust when it promises you, unlike us evil Humans.
Rewriting our example using promises.
Lets skip serving the cake (yes we are the wild ones who eat directly off the stove)
const bakeCake = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Cake is baked');
resolve();
}, 2000);
});
};
const eatCake = () => {
setTimeout(() => {
console.log('Cake is eaten');
}, 2000);
};
bakeCake().then(eatCake);
What we did here is instead of executing the bakeCake
function normally, we are enclosing it in a Promised environment. Previously we did not return anything in bakeCake
but now we are returning a Promise to the callee.
A promise that the piece of code enclosed is executed with an assurance that once its completed, either successfully or broke down due to some abnormality, you will be notified.
resolve
being the indicator of success and
reject
for any abnormal execution (mostly for an error)
In our case of bakeCake
we are resolving the promise ( notifying the callee that the piece of code which was promised to be supervised has completed successfully) and on the callee's side we can listen to the notification with then
and the abnormalities with catch
which we haven't covered here.
Promises enable chaining which is not possible by callbacks.
Suppose we had to log our cake baking. We could chain our functions as
const bakeCake = (cakeLog) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Cake is baked');
cakeLog.push('Perfectly baked!')
resolve(cakeLog);
}, 2000);
});
};
const serveCake = (cakeLog) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Cake is served');
cakeLog.push('Served Well');
resolve(cakeLog);
}, 2000);
});
};
const eatCake = (cakeLog) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Cake is eaten');
cakeLog.push('Ate like its the last cake on earth')
resolve(cakeLog);
}, 2000);
});
};
bakeCake([])
.then(serveCake)
.then(eatCake)
.then(console.log);
We pass in an empty array []
to bakeCake
and when it resolves it pushes its own log statement into the array then reaches the first then
when resolved and the function
you pass as the parameter to then
gets the parameter as the content you passed into the resolve
call.
To understand better. We can rewrite the function calls as
let cakeLog = [];
bakeCake(cakeLog).then(cakeLog => {
serveCake(cakeLog).then(cakeLog => {
eatCake(cakeLog).then(cakeLog => {
console.log(cakeLog);
});
});
});
We pass cakeLog
into bakeCake
and we get it back (after getting updated in the cakeLog) as a parameter to the function we pass in to the then
call. So we can pass it along to serveCake
and repeat the same till we need to consume the accumulated/gross data.
It makes more sense when we correlate to a actual scenario like
let userID = 1001;
getUser(userID)
.then((user) => getPosts(user))
.then((posts) => getTotalLikes(posts))
.then((likeCount) => console.log(likeCount));
But We always want better.
async - await
async - await enable us to write asynchronous code just like how we would write synchronous code by acting as a syntactical sugar to the powerful Promise pattern.
A blueprint of using async await with respect to the underlying Promise pattern would be
async function(){
let paramYouSendIntoResolve = await promReturningFn();
}
- Call your asynchronous function but use an await keyword before it
- Instead of passing in a function to capture the resolved data. Take it as a return value of the function. Wow
- Just one minor discomfort. As you are doing asynchronous stuff amidst of ever synchronous JS flow. Just append async to the function where you use await so that JS Engine knows you are going to do async stuff and interprets accordingly because it has to turn them into Promises later.
Back to our cake baking. (excluded the logging stuff)
const bakeCake = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Cake is baked');
resolve();
}, 2000);
});
};
const serveCake = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Cake is served');
resolve();
}, 2000);
});
};
const eatCake = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Cake is eaten');
resolve();
}, 2000);
});
};
(async ()=>{
await bakeCake();
await serveCake();
await eatCake();
})();
Notice that we have used an IIFE here to force async function execution.
There we are!
We have reached the ability to call asynchronous functions Asif as if they were synchronous.
Thanks for reading. I hope you got something out of this
Top comments (1)
Very nice and well-written article!