A Deep Dive into Promises and Asynchronous JavaScript
Advanced JavaScript Concepts You Shouldn’t Miss​
It’s been a while since I have worked with JavaScript and I am still learning some newer concepts day by day. Starting with that, here are some advanced JavaScript concepts you shouldn’t miss learning as a developer. Whether you are working with Python, C, or maybe Golang, JavaScript is everywhere, and your chances of interacting with it shortly are high. So let’s dive deep into understanding different concepts.
Promises in JavaScript​
Hearing about promises, what do you think at first? As the name suggests, it’s a kind of promise that JavaScript makes to us. Enough of these layman talks; let’s dive deep into this. To learn about Promises, we first need to understand asynchronous and synchronous concepts. It’s the backbone of learning promises.
Synchronous​
It’s a common way how programming languages generally work. It first executes the first line of code, then the second, and then proceeds to the output.
We can take a simple code snippet to understand what’s happening in the code.
let a = 1;
let b = 2;
let c = a + b;
console.log(c);
This code is simple, yet we can understand what happens with it. This code runs line by line and executes in a sequential manner.
Asynchronous​
Asynchronous is a rather concept which is not in a sequential manner or not synchronous. Let’s look at the diagram to understand better.
In Asynchronous as we can see the Process A is fetching data so it’s taking longer time so the next process B will start executing. When the execution of Process A completes then we created a callback and then the Process C and D executes.
// Process A
function processA() {
console.log('Process A: Initiating asynchronous operation');
const data = getSomeDataFromServer();
return data;
}
// Simulating an asynchronous operation
function getSomeDataFromServer() {
return new Promise((resolve) => {
setTimeout(() => {
const data = 'Data from server';
console.log('Process A: Asynchronous operation completed');
resolve(data);
}, 2000); // Simulating a 2-second delay
});
}
// Process B
function processB() {
const b = 2;
console.log('Process B:', b);
}
// Process C
function processC(a) {
const c = a + 2;
console.log('Process C:', c);
}
// Process D
function processD(c) {
console.log('Process D: Result:', c);
}
// Executing the processes
console.log('Application started');
const dataPromise = processA();
processB();
dataPromise.then((a) => {
processC(a);
processD(a + 2);
});
console.log('Application continued execution');
The output will look like this:
Here’s an example with the Fetch API:
async function fetchData() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data = await response.json();
return data;
}
console.log(fetchData());
This will return Promise { <pending> }
as the output.
What is a Promise?​
A Promise is a way to handle asynchronous operations. A promise can have three states:
- Pending
- Fulfilled
- Rejected
Sample Promise:
let somePromise = new Promise((resolve, reject) => {
if(something) {
resolve("It's good resolved");
} else {
reject("Rejected");
}
});
Using a Promise​
- .then()
- .catch()
- .finally()
a) Promise Chaining​
Promise chaining enables sequential execution of asynchronous tasks. This involves returning a promise from within a .then()
callback, allowing subsequent .then()
calls to be chained together.
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => processData(data))
.then(result => displayResult(result))
.catch(error => handleError(error));
b) Creating Custom Promises​
You can create your own promises using the Promise constructor. This is useful when working with functions that perform asynchronous tasks.
function fetchData() {
return new Promise((resolve, reject) => {
// Perform asynchronous operation
if (/* operation successful */) {
resolve(data);
} else {
reject(error);
}
});
}
c) Advanced Promise Operations​
- Promise.all(): Resolves an array of promises concurrently and returns a single promise that resolves when all of the input promises have resolved or any one of them rejects.
- Promise.race(): Returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects.
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
throw new Error('Failed to fetch data');
}
}
d) Real-World Use Cases​
- API Calls
- User Authentication
- File Uploads/Downloads
e) Best Practices​
- Handle Errors Properly: Always handle errors using
.catch()
ortry...catch
to prevent uncaught promise rejections. - Avoid Callback Hell:
function getWater(callback) {
// Simulate fetching water asynchronously
setTimeout(() => {
const water = 'water';
callback(water);
}, 1000);
}
function boilWater(water, callback) {
// Simulate boiling water asynchronously
setTimeout(() => {
const boiledWater = 'boiled water';
callback(boiledWater);
}, 2000);
}
function getTeaLeaves(callback) {
// Simulate getting tea leaves asynchronously
setTimeout(() => {
const teaLeaves = 'tea leaves';
callback(teaLeaves);
}, 1000);
}
function steepTea(boiledWater, teaLeaves, callback) {
// Simulate steeping tea asynchronously
setTimeout(() => {
const tea = 'tea';
callback(tea);
}, 2000);
}
function pourTea(tea, callback) {
// Simulate pouring tea asynchronously
setTimeout(() => {
const teaCup = 'tea cup';
callback(teaCup);
}, 1000);
}
getWater(water => {
boilWater(water, boiledWater => {
getTeaLeaves(teaLeaves => {
steepTea(boiledWater, teaLeaves, tea => {
pourTea(tea, teaCup => {
console.log('Here is your tea:', teaCup);
});
});
});
});
});
Solution:
async function makeTea() {
try {
const water = await getWater();
const boiledWater = await boilWater(water);
const teaLeaves = await getTeaLeaves();
const tea = await steepTea(boiledWater, teaLeaves);
const teaCup = await pourTea(tea);
console.log('Here is your tea:', teaCup);
} catch (error) {
console.error('Error making tea:', error);
}
}
makeTea();
Or using .then()
chaining:
getWater()
.then(water => {
return boilWater(water);
})
.then(boiledWater => {
return getTeaLeaves().then(teaLeaves => [boiledWater, teaLeaves]);
})
.then(([boiledWater, teaLeaves]) => {
return steepTea(boiledWater, teaLeaves);
})
.then(tea => {
return pourTea(tea);
})
.then(teaCup => {
console.log('Here is your tea:', teaCup);
})
.catch(error => {
console.error('Error making tea:', error);
});
Example of Using Fetch Operations:
To resolve this promise, we can use .then()
like this:
fetchData().then(data => console.log(data));
Output:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\n" +
"suscipit recusandae consequuntur expedita et cum\n" +
"reprehenderit molestiae ut ut quas totam\n" +
"nostrum rerum est autem sunt rem eveniet architecto"
}
We can additionally use chaining to catch some errors. The .finally()
will run regardless of whether there was an error or success.
fetchData().then(data => console.log(data)).catch(err => console.log(err)).finally(() => console.log('done'));
Alternatively, we can do it directly like this too:
fetch('https://jsonplaceholder.typicode.com/posts/1').then(response => {
return response.json();
}).then(data => {
console.log(data);
}).catch(error => {
console.log(error);
}).finally(() => {
console.log('finally');
});
Conclusion​
Hopefully, you now have a good understanding of Promises and asynchronous and synchronous concepts in general. Thank you! If you love my work, be sure to follow me.