Understanding JavaScript Promises: The JavaScript CID Bureau.

In our childhood, most of us read crime thrillers. My favorite show at that time was CID.
In JavaScript, when we write code, it feels like a new CID case has arrived. Most of the time, CID gives priority to certain cases because we all know some cases are more demanding, like a serial killer case, while others, like a robbery, may receive less priority.
JavaScript works similarly. Some tasks have higher priority, so JavaScript handles them immediately. Less demanding tasks are outsourced to the Web APIs. Because of this, the normal flow of code execution changes a little, and we get JavaScript’s asynchronous nature.
Promises: Every CID Case Has a Status
A Promise is a special JavaScript object that represents something that will happen in the future.
In JavaScript, sometimes we need to perform a task that takes time to complete. However, we don’t want to freeze the normal flow of execution because that would make the user experience slow and unresponsive. JavaScript solves this using its asynchronous features (for example, fetching API data through a network request in the browser).
Promises help us achieve this. Since the final result is not immediately available, JavaScript returns a Promise rather than the final value.
A Promise has three states:
Pending – In this state, the Promise internal operation is not completed.
Resolved (Fulfilled) – The task completed successfully.
Rejected – The task failed; this throws errors.
Just like in CID, a case may be pending, may be solved, or may remain unsolved.
Why do we need Promises (History)
Before ECMAScript 2015 (ES6) introduced native Promises, JavaScript handled asynchronous programming entirely through callbacks. A callback is simply a function passed to another function that is executed later when a task finishes.
Imagine a new case arrives at the CID bureau. ACP Pradyuman assigns Abhijeet to investigate. Abhijeet says he will proceed only after Freddy checks the CCTV footage. Freddy says he can check the footage only after the technician restores the system. The technician says he will fix the system after receiving approval from another department.
So here the problem is that no one can proceed if the other person does not complete their task first. Everything is connected like a chain. Before asynchronous JavaScript, if we needed this behavior, the code would become complicated and verbose.
function assignCaseCB(caseName, cb) {
setTimeout(() => cb(null, { caseName, status: "assigned by ACP" }), 100);
}
function investigateCaseCB(caseDetails, cb) {
setTimeout(() => cb(null, { ...caseDetails, status: "investigating by Abhijeet" }), 100);
}
function checkCCTVCB(caseDetails, cb) {
setTimeout(() => cb(null, { ...caseDetails, status: "CCTV checked by Freddy" }), 100);
}
function restoreSystemCB(caseDetails, cb) {
setTimeout(() => cb(null, { ...caseDetails, status: "system restored by Technician" }), 100);
}
function approveCaseCB(caseDetails, cb) {
setTimeout(() => cb(null, { ...caseDetails, status: "approved and ready to proceed" }), 100);
}
assignCaseCB("Bank Robbery", (err, caseDetails) => {
if (err) return console.log(err);
investigateCaseCB(caseDetails, (err, caseDetails) => {
if (err) return console.log(err);
checkCCTVCB(caseDetails, (err, caseDetails) => {
if (err) return console.log(err);
restoreSystemCB(caseDetails, (err, caseDetails) => {
if (err) return console.log(err);
approveCaseCB(caseDetails, (err, caseDetails) => {
if (err) return console.log(err);
console.log(`\({caseDetails.caseName}: \){caseDetails.status}`);
});
});
});
});
});
As you can see here, the code is barely understandable. Also, if any phase throws an error, troubleshooting the problem becomes another hard challenge.
How to write promises in JavaScript
In JavaScript, we create a Promise using an Promise object, which takes a callback function. This callback function receives two parameters: resolve and reject. Inside the Promise, we call resolve() when the task is successful and reject() when an error occurs.
Syntax
let res=true;
let promise = new Promise((res, rej) => {
setTimeout(() => {
if(res) res("Promise is resolved");
rej(new Error("Promise is rejected"));
}, 2000);
});
As you can see in the above example, there is a resource variable set to true. On the next line, we create a Promise with a callback that contains resolve and reject. Inside the callback, we use setTimeout(), and if resource is true, we resolve the Promise; otherwise, we reject it after a delay of 2000 milliseconds.
To consume this promise, we have then() and catch().
To understand then() and catch(), We can think of it like this: when a Promise is created, it acts like ACP Pradyuman, who says, “We always deliver justice.” Then we have Abhijeet, who represents then(), and Daya, who represents catch().
In this situation, Abhijeet receives the criminal caught by ACP Pradyuman (the resolved value of the Promise). Abhijeet tries to calmly extract the truth from the criminal. If he is not able to find the truth, the case is passed to Daya — and we all know how Daya handles things.
Similarly, in JavaScript, when a Promise is resolved, then() takes a callback function and receives the resolved value. If the Promise is rejected and throws an error, it can also be handled inside then() because then() accepts a second callback for errors. However, nowadays, we usually use catch() (our Daya) to handle errors.
promise.then(
(data) => console.log(data),
(err) => console.log(err.message),
); // Note that then can handle the error also
promise
.then((data) => data.toUpperCase())
.then(console.log) // we can chain .then() for process the value
.catch((err) => console.log(err.message));
As we can see here, the syntax is much cleaner and less verbose compared to the older callback chaining approach. Also, we have better control over how we operate on data, and error handling and troubleshooting become much easier.
Event Chain .then(): Error propagation
If we write a Promise and consume it using then(), we can chain multiple .then() Methods to process data step by step. However, if the Promise gets rejected, the error is automatically passed to the next .then(). If the error is not handled there, it continues to propagate down the chain until it reaches a .catch() block that properly handles the error.
Event Loop: Case Priority.
To understand the Event Loop, it is very important to first understand the execution flow of normal code and Promises.
In CID, when the team goes to a criminal’s house to catch him, normal inspectors like Abhijeet and Freddy try to find a way to enter, such as breaking a glass window, because the house is locked. But if Daya suddenly arrives at the scene, we all know what happens — he breaks down the door immediately with high priority and gets the job done.
In JavaScript, something similar happens. We have synchronous code that runs on the Call Stack with the highest priority. Then we have the Microtask Queue, which has the second-highest priority, and finally the Macrotask Queue, which has lower priority.
Call Stack: Where synchronous code executes sequentially.
Macrotask Queue: Holds callbacks originating from
setTimeout,setIntervaletc.Microtask Queue: It holds Promises.
Priority Order is CallStack > Microtask > Macrotask
The Event Loop is responsible for executing tasks. It decides which code should be executed at a given time and manages the overall flow of execution.
Flow Diagram of Event Loop
console.log("1 Case Registered (Synchronous - Call Stack)");
setTimeout(() => {
console.log("2 Macrotask: setTimeout executed");
}, 0);
Promise.resolve().then(() => {
console.log("3 Microtask: Promise resolved");
});
console.log("4 Investigation Started (Synchronous - Call Stack)");
/* Output
1 Case Registered (Synchronous - Call Stack)
4 Investigation Started (Synchronous - Call Stack)
3 Microtask: Promise resolved
2 Macrotask: setTimeout executed
*/
All Promise Static Methods
To understand the static methods easily, let’s take the CID case example again. Assume the CID team is trying to solve a case, and ACP sir assigns three officers — Abhijeet, Freddy, and Daya — to catch the criminal.
Now, let’s discuss all the available JavaScript static methods we have for Promises.
All()
Any()
AllSettled()
Race()
Resolve()
Reject()
These are the most used ones.
All() Method
Assume it like a CID investigation where officers go on a mission. Three officers are assigned to catch three criminals together. If any one of them fails to catch their criminal, the entire mission fails.
Promise.all() JavaScript works in a similar way. It receives multiple promises as an array, and if all the promises are successful, it returns all their results. However, if even one promise fails, it immediately rejects everything.
const officer1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("Criminal 1 caught"), 1000);
});
const officer2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("Criminal 2 caught"), 1500);
});
const officer3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("Criminal 3 caught"), 2000);
});
Promise.all([officer1, officer2, officer3])
.then((results) => {
console.log("Mission Success");
console.log(results);
})
.catch((error) => {
console.log("Mission Failed");
console.log(error);
});
/* Output
Mission Success
[ 'Criminal 1 caught', 'Criminal 2 caught', 'Criminal 3 caught' ]
*/
Any() Method
Here, the scenario changes a little. Now, all three officers go to catch three criminals. However, if even one officer successfully catches a criminal, that criminal is brought to ACP, sir. After interrogation, they reveal the locations of the other criminals, and the overall mission is considered a success.
In JavaScript, this works similarly to Promise.any(). It receives an array of promises, and if at least one of them is successfully resolved, the whole Promise is resolved. It only rejects if all the promises fail.
const officer1 = new Promise((resolve, reject) => {
setTimeout(() => reject("Officer 1 failed"), 1000);
});
const officer2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("Criminal caught by Officer 2"), 1500);
});
const officer3 = new Promise((resolve, reject) => {
setTimeout(() => reject("Officer 3 failed"), 2000);
});
Promise.any([officer1, officer2, officer3])
.then((result) => {
console.log("Mission Success");
console.log(result);
})
.catch((error) => {
console.log("Mission Failed");
console.log(error);
});
/* Output
Mission Success
*/
Race() Method
In this scenario, all three officers try to catch the criminal, but whoever catches the criminal first hands them over to ACP Sir, and the mission is considered successful.
In JavaScript, Promise.race() settles as soon as the first Promise settles (either resolves or rejects).
const officer1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("Criminal caught by Officer 1"), 2000);
});
const officer2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("Criminal caught by Officer 2"), 1000);
});
const officer3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("Criminal caught by Officer 3"), 3000);
});
Promise.race([officer1, officer2, officer3])
.then((result) => {
console.log("Mission Result:");
console.log(result);
})
.catch((error) => {
console.log("Mission Failed:");
console.log(error);
});
/* Output
Mission Success
Criminal caught by Officer 2
*/
allSettled() Method
In this scenario, ACP Sir gives strict instructions to all three officers that, whether the mission fails or succeeds, he wants a full and final report. So, whether the officers catch the criminal or not, they must report back to ACP sir. In the end, ACP Sir receives the complete report from everyone.
In JavaScript, Promise.allSettled() works in a similar way. It returns the detailed result of all the promises it receives as parameters, whether they are fulfilled or rejected.
const officer1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("Officer 1 caught the criminal"), 1000);
});
const officer2 = new Promise((resolve, reject) => {
setTimeout(() => reject("Officer 2 failed"), 1500);
});
const officer3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("Officer 3 caught the criminal"), 2000);
});
Promise.allSettled([officer1, officer2, officer3])
.then((results) => {
console.log("Final Report:");
console.log(results);
});
/* Output
Final Report:
[
{ status: 'fulfilled', value: 'Officer 1 caught the criminal' },
{ status: 'rejected', reason: 'Officer 2 failed' },
{ status: 'fulfilled', value: 'Officer 3 caught the criminal' }
]
/*
Promise.resolve()
Promise.resolve() creates a Promise that is already fulfilled. It does not wait for anything and immediately returns a successful result.
To understand this, imagine a criminal who thinks his plan is perfect and that no one will catch him, but CID has already gathered all the evidence against him. This means the case is already solved.
const caseStatus = Promise.resolve("Case already solved. Criminal arrested.");
caseStatus.then((message) => {
console.log(message);
});
// Output Case already solved. Criminal arrested.
Promise.reject()
Promise.reject() creates a Promise that is already rejected. It immediately returns a failed result without waiting for anything.
To understand this, imagine a situation where ACP Sir receives confirmed information that the criminal has escaped before the team even starts the mission. The case fails instantly — no investigation is needed.
const caseStatus = Promise.reject("Mission failed. Criminal escaped.");
caseStatus
.then((message) => {
console.log(message);
})
.catch((error) => {
console.log(error);
});
// Output Mission failed. Criminal escaped.
Conclusion
JavaScript Promises make asynchronous programming more structured, organized, and easy to understand. Just like CID sir’s investigation, every task has its own status, priority, and execution flow. We don’t need to mess around with deeply nested callbacks because Promises make asynchronous programming more readable, clean, and organized.
We can handle both the success and failure scenarios of the asynchronous task with the help of the then() and catch() methods, respectively. We know that the Event Loop is the main component that makes JavaScript handle asynchronous programming with the help of the Call Stack, Microtask Queue, and Macrotask Queue, giving us priority for the asynchronous task execution. And with the static methods like Promise.all(), Promise.any(), Promise.race(), Promise.allSettled(), we can easily handle multiple asynchronous task executions.
In simple words, Promises make JavaScript handle asynchronous programming like ACP sir handling different officers during the mission execution, i.e., with priority, order, and results.
As long as we know the Promises, JavaScript’s asynchronous programming is not confusing anymore, but it becomes more organized, structured, and powerful.




