Asynchronous JavaScript: The Restaurant Analogy

This article is intended for beginners who are starting off with JavaScript and trying to understand the working of JavaScript Runtime. This will be a good refresher for experienced professionals as well.
This is my humble attempt to oversimplify the asynchronous nature of JavaScript using the Restaurant Analogy. Feel free to correct me in the comments if I have made any mistakes.
Need For Asynchronous Execution
JavaScript is a synchronous, single-threaded programming language. Its code is executed sequentially using a single call stack within a single thread. But it is not possible to do all operations sequentially.
Suppose we want to wait for some data or input to be generated during a sequential run. Since JavaScript is single-threaded, making the code wait on some operation may result in freezing the whole application. These kinds of scenarios can be avoided by offloading such time-consuming operations from the main thread. JavaScript Runtime Environments(Browsers/NodeJS) provide a set of external APIs for handling such operations.

The above image shows the basic architecture of the JavaScript Runtime Environment. It includes the following parts:
- JavaScript Engine ⚙️
- Web APIs 🔴🔴🔴
- Task Queue
🟦🔣🔠🔢🔡🔤
- Event Loop 🔄
All these elements form the basic building blocks of Asynchronous JavaScript.
If Web Browser was a Restaurant

Consider the web browser as a Restaurant. It consists of a dining hall and a kitchen.
The Dining Hall is the place where customers come in and place orders. This is analogous to a user loading a Web Page and performing some operation like fetching data from the server.
The Kitchen is the place where all the magic happens. This is analogous to the JavaScript Runtime Environment which consists of different components, each performing a separate task.
These components are listed below.
🤵Waiter(JavaScript Engine): The waiter performs the task of running around taking orders and submitting it to the kitchen, serving food and arranging the tables. The waiter is also part of the kitchen even though he is shown outside in the figure. This is analogous to the JavaScript Engine that runs the code.
👨🍳Chefs(Web APIs): The chefs take the order from the waiter, prepare the item and put it down on the delivery desk. This is analogous to the Web APIs performing the task requested by the user and pushes the result to the task queue.
👨💼Take-Away Manager(Event Loop): His job is to manage the takeaway counter. He performs two main tasks repeatedly.
1. Check if there are items on the delivery desk.
2. Check the availability of the waiter and call him whenever he is free.
This is analogous to the Event Loop whose job is to:
1. Check if there are items in the Task Queue
2. Check the status of the call stack within the JavaScript Engine and push items from the queue if it is empty.
🍽️Delivery Desk(Task Queue): This is the desk on which the chefs place the prepared dishes, so that the takeaway manager can pass it on to the waiter when available. This is analogous to the Task Queues present in the JavaScript Runtime.
Placing an Order
Each order from a customer can be equated to an API call. When a customer makes an order, the waiter🤵 notes it down and passes it to the respective chef👨🍳 in the kitchen.
If the restaurant was working Synchronously, the waiter🤵would be waiting in the kitchen until the order is done, so that it can be delivered to the customer.
This is a problem😑. While the food is being prepared, the dining hall operations of the restaurant will be halted🚫. There will be no new orders taken, no food served or no tables arranged.
This can be avoided if the restaurant is functioning in an Asynchronous manner.
The waiter🤵 now passes the order to the kitchen and continues with his tasks while the chef👨🍳 prepare the dish. Once the dish is prepared, the chef👨🍳 puts it down on the delivery desk🍽️. Now the Take-away manager👨💼 takes over and lets the waiter🤵 know that the particular order is ready. The waiter🤵 can then take the order and serve it to the customer.
As a result, the waiter🤵 and hence the dining hall operations, will not be blocked while the food is being prepared.
Callbacks
JavaScript handles such operations using Callbacks📞. When the order is passed to the kitchen, we also pass a callback📞 along with it. This callback holds a note📝 saying 'Serve order to table X'
. When the order is ready the chef👨🍳 places the prepared dish along with the callback note📝 on the delivery desk🍽️. The Take-away manager👨💼 then makes a call📞 to the waiter🤵 for that particular order. The waiter🤵 receives the item, reads the note📝 and serves the item on table X.
Representing the restaurant functions through code
The following functions are responsible for fulfilling each order that comes in.
prepareItem(itemName, callback): This function takes in an itemName🍔 and callback📞 as parameters. Preparing the item is simulated using the setTimeout⏰ API which fires after the specified timeout⏳(Preparation Time). The preparation time is chosen using the itemName and the timeout is set based on it. Once the setTimeout⏰ fires, it calls the passed in callback function.
function prepareItem(itemName, callback){
Let preparationTime = 0;
switch(itemName){
case "coffee☕": preparationTime = 4000; break;
case "soda🥤": preparationTime = 2000; break;
case "frenchFries🍟": preparationTime = 5000; break;
case "hamburger🍔": preparationTime = 3000; break;
case "sandwich🥪": preparationTime = 6000; break;
default: preparationTime = 1000;
}
setTimeout(()=>{
console.log(itemName + 'Prepared');
callback();
},preparationTime);
}function serveOrder(){
console.log('Order Served');
}
serveOrder(): This function prints
'Order Served'
on the console 💻. It is supposed to be called once items in the order are done preparing.
function serveOrder(){
console.log('Order Served');
}
The prepareItem() function can now be called by passing "coffee☕"
as the itemName and the serveOrder() method as callback (note 📝)
prepareItem("coffee☕", serveOrder);
This function call can be wrapped in a submitOrder() function for better modularity.
function submitOrder(){
prepareItem("coffee☕", serveOrder);
}
submitOrder();
This works fine, but is not enough🤨. What if the customer makes an order that contains three related items.
["hamburger🍔", "frenchFries🍟", "soda🥤"]
Any one of these three items, if served separately, is useless. It needs to be served together. This now leads to a new problem🙄.
If the waiter🤵 places order for all the three items together, the submitOrder() function looks like this:
function submitOrder(){
prepareItem("hamburger🍔", serveOrder);
prepareItem("frenchFries🍟", serveOrder);
prepareItem("soda🥤", serveOrder);
}
submitOrder();
This will not produce the desired results😕 as the items may arrive out of order.
Nested Callbacks
This issue can be resolved if nested callbacks📞📞📞
are used.
The waiter🤵 submits the order for "hamburger🍔"
to the respective chef👨🍳 and passes a callback along with it having the following notes📝
* Submit Order for "frenchFries🍟"
* Submit Order for "soda🥤"
* Serve items to table X
Once the order gets completed, the chef👨🍳 places it onto the delivery desk🍽️ and the take-away manager👨💼 makes a call📞 for the waiter🤵 as usual.
The waiter🤵 removes the note📝 at the top, reads it and submits the order for the "frenchFries🍟"
as per the note. The "hamburger🍔"
is not delivered immediately. The order for "frenchFries🍟"
is placed along with the remaining callback notes📝.
* Submit Order for "soda🥤"
* Serve items to table X
Once the second order gets completed, the item arrives at the delivery desk 🍽️ and the waiter🤵 is called📞 again. Now, the waiter🤵 removes the note📝 at the top, reads it and submits the order for "soda🥤"
. The "hamburger🍔"
and "frenchFries🍟"
are not delivered yet. This order is placed along with the last callback note📝.
* Serve items to table X
When the order gets completed, the item arrives at the delivery desk🍽️ and the waiter🤵 is called📞 once again. Now, the callback note📝 says to serve the order. Hence, he serves all the items on table X together.
The submitOrder() function using Nested Callbacks is as follows.
function submitOrder(){
prepareItem("hamburger🍔", ()=>{
prepareItem("frenchFries🍟", ()=>{
prepareItem("soda🥤", serveOrder);
});
});
}
submitOrder();

The above sequence diagram shows the timing between orders for three different items.
This order of three items is itself quite overwhelming right?😅 What if we had a few more items. The submitOrder() function in this case may look somewhat like this😵.
function submitOrder(){
prepareItem("coffee☕", ()=> {
prepareItem("soda🥤", ()=> {
prepareItem("frenchFries🍟", ()=> {
prepareItem("hamburger🍔", ()=> {
prepareItem("sandwitch🥪", serveOrder);
});
});
});
});
}
submitOrder();
The readability of such code reduces after each nesting and becomes difficult to maintain. This kind of code is often referred to as ‘The Callback Hell’👹.
Promises
The ES6 version of JavaScript introduced Promises🤝 to handle such issues. A Promise is an object representing the eventual completion or failure of an asynchronous operation.
While using promises, the waiter🤵 gets a promise🤝 back when an order is placed. No callback note is passed to the kitchen in this case. The waiter🤵 then attaches a note📝 that says, what needs to be done when the promise is resolved, to the promise🤝 and keeps it with himself. In our example, the attached note will be:
* Serve item to table X
When the order is ready the chef👨🍳 places it on to the delivery desk🍽️ along with the 'Promise Resolved ✅'
message.
The take-away manager👨💼 then hands over the order to the waiter🤵. He then finds the matching promise🤝 from his set of promises, reads the attached note📝 and serves the item on table X.
Promise code implementation
We can rewrite the prepareItem() function to return a promise🤝 as follows.
function prepareItem(item){
let preparationTime = 0;
switch(item){
case "coffee☕": preparationTime = 4000; break;
case "soda🥤": preparationTime = 2000; break;
case "frenchFries🍟": preparationTime = 5000; break;
case "hamburger🍔": preparationTime = 3000; break;
case "sandwich🥪": preparationTime = 6000; break;
default: preparationTime = 1000;
}
return new Promise((resolve, reject) => {
setTimeout(()=>{
console.log(item + 'Prepared');
resolve();
},preparationTime);
});
}
The function no longer takes a callback function as the argument, but now returns a new Promise object. The promise takes an anonymous function having resolve✅ and reject❌ as arguments. Instead of calling a callback function, we can call resolve() if the operation is successful or reject() if there is an error.
Using the above implementation of prepareItem(), the submitOrder() function for a single item can be written as follows.
function submitOrder(){
let res = prepareItem("coffee☕");
res.then(serveOrder);
}
submitOrder();

Promise chaining:
Promise chaining 🤝🔗🤝🔗🤝
is a solution to the callback hell situation caused by nested callbacks.
The ‘then()’ call on a promise response takes a function as argument. This callback function may return another Promise🤝. The response of this new promise🤝 can be accessed by chaining ⛓‘then()’ calls.
The submitOrder() function using a sequence of promises can look like this:
function submitOrder() {
let res1 = prepareItem("hamburger🍔");
let res2 = res1.then(() => {
return prepareItem("frenchFries🍟");
});
let res3 = res2.then(() => {
return prepareItem("soda🥤");
});
res3.then(serveOrder);
}
submitOrder();
This can be further simplified by removing the return keyword as follows:
function submitOrder() {
let res1 = prepareItem("hamburger🍔");
let res2 = res1.then(() => prepareItem("frenchFries🍟"));
let res3 = res2.then(() => prepareItem("soda🥤"));
res3.then(serveOrder);
}
submitOrder();
Assignment to separate response variables can be removed by chaining⛓ ‘then()’ calls as follows:
function submitOrder() {
prepareItem("hamburger🍔")
.then(() => prepareItem("frenchFries🍟"))
.then(() => prepareItem("soda🥤"))
.then(serveOrder);
}
submitOrder();
This looks more clean and readable than using nested callbacks.

When there are more items in the order, the submitOrder() function can look as follows:
function submitOrder(){
prepareItem("coffee☕")
.then(() => prepareItem("soda🥤"))
.then(() => prepareItem("frenchFries🍟"))
.then(() => prepareItem("hamburger🍔"))
.then(() => prepareItem("sandwich🥪"))
.then(serveOrder);
}
submitOrder();
Async/Await
The async/await syntax was added to JavaScript in 2017. This is basically syntactic sugar🍚 for Promises🤝 which makes the asynchronous code look more sequential and makes the code easier to read.
The submitOrder() function can be written using this async/await syntax as follows:
async function submitOrder(){
await prepareItem("coffee☕");
await prepareItem("soda🥤");
await prepareItem("frenchFries🍟");
await prepareItem("hamburger🍔");
await prepareItem("sandwich🥪");
completeOrder();
}
submitOrder();
The await calls need to be wrapped inside an async function. So, the submitOrder() function is now marked as async. This can be called just like any other function.
The code now looks much cleaner and readable. It appears to be sequential when it is actually an abstraction for promises.
Conclusion
To wrap things up, we looked at how we can model the asynchronous behavior of JavaScript in terms of the working of a restaurant🤵👨🍳👨💼🍽️. We then looked at the different approaches JavaScript uses to model this asynchronous behavior using the restaurant code example.
We went through the difference between the Callback based approach and Promise based approach using the restaurant analogy. We also mentioned how to convert the promise-based code to async/await syntax.
More content at plainenglish.io. Sign up for our free weekly newsletter here.