Promises And Async/Await in JavaScript
JavaScript is a single threaded, Synchronous language, basically it means until the previous task is completed the next task will not be completed.
While there are times where we need task to be executed at same time which is also called Asynchronous operations.
Here is an example of synchronous and Asynchronous operation
//Synchronous Code execution
console.log(1);
console.log(2);
console.log(3);
// Asynchronous Code execution
console.log(1);
// This will be shown after 2 seconds
setTimeout(()=>{
console.log(2);
},2000)
console.log(3)
CallBacks
When you nest a function inside another function as an argument, that’s called a callback.
Callbacks are generally used when we need to execute a process step by step, i.e you want to execute a certain block of code after a specific task is performed.
Here is an example
import React from "react";
const Test = () => {
const fetchData = (callback) => {
// adding an asynchronous fetch operation
setTimeout(() => {
const data = { message: "Data fetched successfully!" };
callback(data); // callback function with the fetched data is called
}, 2000);
};
const handleData = (data) => {
console.log(data.message);
};
const handleClick = () => {
fetchData(handleData); // callback function is passed here
};
return (
<button onClick={handleClick}>Fetch Data</button>
);
};
export default Test;
Now the on of the issues with the callbacks are , Callback Hell, this happens when you have multiple tasks dependent on each other
lets take an example,
const fetchData = (callback) => {
setTimeout(() => {
const data = { message: "Data fetched successfully!" };
callback(data);
}, 2000);
};
const processData = (data, callback) => {
setTimeout(() => {
const processedData = data.message.toUpperCase();
callback(processedData);
}, 2000);
};
const displayResult = (result) => {
console.log(result);
};
fetchData((data) => {
processData(data, (processedData) => {
displayResult(processedData);
});
});
In this example, we have two functions: fetchData
and processData
. fetchData
simulates an asynchronous operation to fetch data, and processData
simulates an operation to process the fetched data. Both functions take a callback function as an argument.
To demonstrate callback hell, we call fetchData
and provide a callback function. Inside the fetchData
callback, we call processData
and provide another callback function. Finally, inside the processData
callback, we call displayResult
.
As you can see, the code structure becomes nested and hard to read due to the callbacks. Each callback relies on the completion of the previous operation. If you have more complex code with multiple callbacks, the indentation and complexity can increase, leading to callback hell.
Promises
Imagine you’re ordering a pizza from a restaurant. Instead of waiting at the counter until your pizza is ready, you’re given a “promise” that your pizza will be delivered to your doorstep when it’s ready.
In programming, Promises work in a similar way. They represent a promise that something will happen in the future, usually an asynchronous operation like fetching data from an API or reading a file. Instead of blocking the program and waiting for the operation to complete, Promises allow us to continue with other tasks and handle the result later.
Example
const fetchPizza = new Promise((resolve, reject) => {
const pizzaIsReady = true;
if (pizzaIsReady) {
resolve("Delicious pizza!");
} else {
reject("Oops! Something went wrong.");
}
});
fetchPizza
.then((pizza) => {
console.log(pizza);
})
.catch((error) => {
console.error(error);
});
As the above charts show, a promise has three states:
- Pending: This is the initial stage. Nothing happens here.
- Resolved: f the job is finished successfully, it returns a result.
- Rejected: if an error has occurred.Error object is returned
Here, are basic examples of resolved and rejected operations
let promise = new Promise(function(resolve, reject) {
// the function is executed automatically when the promise is constructed
// after 1 second signal that the job is done with the result "done"
setTimeout(() => resolve("done"), 1000);
});
let promise = new Promise(function(resolve, reject) {
// after 1 second signal that the job is finished with an error
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
Note :The executor should call only one
resolve
or onereject
. Any state change is final.All further calls ofresolve
andreject
are ignored.resolve
andreject
do not need any argument can be called like without any argument.
The Stare and result are internal, The properties state and result of the Promise object are internal. We can’t directly access them. We can use the methods .then
/.catch
/.finally
for that.
.then
// The first argument of .then is a function that runs when
// the promise is resolved and receives the result.
// The second argument of .then is a function that runs when
// the promise is rejected and receives the error.
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// resolve runs the first function in .then
promise.then(
result => alert(result), // shows "done!" after 1 second
error => alert(error) // doesn't run
);
.catch
If we’re interested only in errors, then we can use null
as the first argument: .then(null, errorHandlingFunction)
. Or we can use .catch(errorHandlingFunction)
, which is exactly the same.
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second
.finally
This handler which works regardless of whether our promise was resolved or rejected.
- It has no arguments
- It “passes through” the result or error to the next suitable handler.
new Promise((resolve, reject) => {
setTimeout(() => resolve("value"), 2000);
})
.finally(() => alert("Promise ready")) // triggers first
.then(result => alert(result)); // .then shows "value"
new Promise((resolve, reject) => {
throw new Error("error");
})
.finally(() => alert("Promise ready")) // triggers first
.catch(err => alert(err)); // .catch shows the error
3. It handler also shouldn’t return anything. If it does, the returned value is silently ignored.
4. If it throws an error, then the execution goes to the nearest error handler.
Promise Chaining
The whole thing works, because every call to a .then
returns a new promise, so that we can call the next .then
on it. so we can add multiple steps after a step is completed. When a handler returns a value, it becomes the result of that promise
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
In above code as you can see if u add any error handler .catch method , it will handle error for all the .then methods, we won’t be able to debug from where the error is generated. it is better to add .catch after each .then
Although , multiple then are available , but what if we need the code to wait and execute a certain function then move forward and delay the execution using setTimeout but next then will proceed so we need to add explicit promise and resolve that to make it wait.
In addition to the basic Promise API, JavaScript also provides some utility functions for working with multiple promises simultaneously. These functions are built on top of the Promise concept and allow you to handle multiple promises in different ways. Here are some of the commonly used utility functions for working with promises:
1. **Promise.all()**:
- `Promise.all(iterable)` takes an iterable (such as an array) of promises and returns a single promise that resolves when all of the input promises have resolved, or rejects if any of the promises is rejected.
```javascript
const promises = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];
Promise.all(promises)
.then(values => console.log('All resolved values:', values))
.catch(error => console.error('At least one promise was rejected:', error));
```
2. **Promise.race()**:
- `Promise.race(iterable)` takes an iterable (such as an array) of promises and returns a single promise that resolves or rejects as soon as one of the input promises resolves or rejects, with the value or reason from that promise.
```javascript
const promises = [Promise.resolve('first'), Promise.resolve('second')];
Promise.race(promises)
.then(value => console.log('First promise to resolve:', value))
.catch(error => console.error('First promise to reject:', error));
```
3. **Promise.allSettled()**:
- `Promise.allSettled(iterable)` takes an iterable (such as an array) of promises and returns a promise that resolves after all the input promises have settled (either resolved or rejected), providing an array of objects with the status and result of each promise.
```javascript
const promises = [Promise.resolve(1), Promise.reject('Error'), Promise.resolve(3)];
Promise.allSettled(promises)
.then(results => console.log('Promise settlement results:', results))
.catch(error => console.error('Error:', error));
```
4. **Promise.any()**:
- `Promise.any(iterable)` takes an iterable (such as an array) of promises and returns a promise that resolves as soon as any of the promises in the iterable resolves. If all promises are rejected, it will reject with an `AggregateError` containing all rejection reasons.
```javascript
const promises = [Promise.reject('Error1'), Promise.reject('Error2'), Promise.resolve('Success')];
Promise.any(promises)
.then(value => console.log('First promise to resolve:', value))
.catch(errors => console.error('All promises rejected:', errors));
```
These utility functions provide convenient ways to handle scenarios involving multiple promises, allowing you to coordinate and manage asynchronous operations effectively.
In addition to the basic Promise API, JavaScript also provides some utility functions for working with multiple promises simultaneously. These functions are built on top of the Promise concept and allow you to handle multiple promises in different ways. Here are some of the commonly used utility functions for working with promises:
1. **Promise.all()**:
- `Promise.all(iterable)` takes an iterable (such as an array) of promises and returns a single promise that resolves when all of the input promises have resolved, or rejects if any of the promises is rejected.
```javascript
const promises = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];
Promise.all(promises)
.then(values => console.log('All resolved values:', values))
.catch(error => console.error('At least one promise was rejected:', error));
```
2. **Promise.race()**:
- `Promise.race(iterable)` takes an iterable (such as an array)
of promises and returns a single promise that resolves or rejects
as soon as one of the input promises resolves or rejects, with the value or reason from that promise.
```javascript
const promises = [Promise.resolve('first'), Promise.resolve('second')];
Promise.race(promises)
.then(value => console.log('First promise to resolve:', value))
.catch(error => console.error('First promise to reject:', error));
```
3. **Promise.allSettled()**:
- `Promise.allSettled(iterable)` takes an iterable (such as an array)
of promises and returns a promise that resolves after all the input promises have settled (either resolved or rejected), providing an array of objects with the status and result of each promise.
```javascript
const promises = [Promise.resolve(1), Promise.reject('Error'),
Promise.resolve(3)];
Promise.allSettled(promises)
.then(results => console.log('Promise settlement results:', results))
.catch(error => console.error('Error:', error));
```
4. **Promise.any()**:
- `Promise.any(iterable)` takes an iterable (such as an array)
of promises and returns a promise that resolves as soon as any
of the promises in the iterable resolves.
If all promises are rejected, it will reject with an `AggregateError`
containing all rejection reasons.
```javascript
const promises = [Promise.reject('Error1'), Promise.reject('Error2'), Promise.resolve('Success')];
Promise.any(promises)
.then(value => console.log('First promise to resolve:', value))
.catch(errors => console.error('All promises rejected:', errors));
```
These utility functions provide convenient ways to handle scenarios involving multiple promises, allowing you to coordinate and manage asynchronous operations effectively.
This is supposed to be the better way to write promises and it helps us keep our code simple and clean.All you have to do is write the word async
before any regular function and it becomes a promise.
async function f() {
return 1;
}
f().then(alert); // 1
// in depth it would work something like this
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
So, async
ensures that the function returns a promise, and wraps non-promises in it.
await
The keyword await
makes JavaScript wait until that promise settles and returns its result, If resolved or the error if rejected
// works only inside async functions
let value = await promise;
await
literally suspends the function execution until the promise settles, and then resumes it with the promise result. That doesn’t cost any CPU resources, because the JavaScript engine can do other jobs in the meantime: execute other scripts, handle events, etc.
It’s just a more elegant syntax of getting the promise result than promise.then
. And, it’s easier to read and write.
To handle error in async/await we can use the promise .catch or try/catch
async function f() {
let response = await fetch('http://no-such-url');
}
// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)
// using try catch
async function f() {
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}
f();
When we use async/await
, we rarely need .then
, because await
handles the waiting for us. And we can use a regular try..catch
instead of .catch
. That’s usually (but not always) more convenient.
But at the top level of the code, when we’re outside any async
function, we’re syntactically unable to use await
, so it’s a normal practice to add .then/catch
to handle the final result or falling-through error, like in the line (*)
of the example above.
If you’d like to be in the know, subscribe, clap, like and share. Cheers!