Promises in Asynchronous JavaScript
Introduction
Promises are a powerful feature introduced in ES6 that allows developers to handle asynchronous code elegantly and improve readability. They provide a way to handle the results of asynchronous operations, such as network requests, and handling multiple callback functions.
Basic Syntax
Promises are created using the Promise
constructor. The constructor takes a single argument, a function called the executor, which is invoked immediately when the promise is created. The executor function takes two arguments, a resolve function, and a reject function, which are used to signal the completion or failure of the asynchronous operation.
const promise = new Promise((resolve, reject) => {
// Asynchronous operation
if (success) {
resolve(result);
} else {
reject(error);
}
});
Once a promise is created, it can be in one of three states:
pending: The initial state, the promise is neither fulfilled nor rejected.
fulfilled: The promise has been completed successfully and the result is available.
rejected: The promise has failed and an error is available.
The Promise Object
Promise objects have several methods that can be used to attach callbacks and handle the results of the asynchronous operation. The most commonly used methods are .then()
and .catch()
.
The .then()
method is used to attach a callback that will be invoked when the promise is fulfilled. It takes a single argument, a function that will be passed the result of any asynchronous operations.
promise.then((result) => {
console.log(result);
});
Furthermore, the .catch()
method is used to attach a callback that will be invoked when the promise is rejected. It takes a single argument, a function that will be passed the error.
promise.catch((error) => {
console.log(error);
});
Promise Chaining
Promise chaining is a technique that allows you to chain multiple .then() and .catch() methods together to handle the results of multiple asynchronous operations. It's worth mentioning that promise chaining can improve the readability and maintainability of the code.
promise
.then((result) => {
// Do something with the result
return new Promise((resolve) => {
resolve(result);
});
})
.then((result) => {
// Do something else with the result
console.log(result);
})
.catch((error) => {
console.log(error);
});
Promisification
Simply put, promistification is transforming a callback-accepting function into a function that returns a promise. Sometimes, this can be useful when working with older libraries that were written before Promises were introduced.
function callbackFunction(arg, callback) {
// Asynchronous operation
callback(result);
}
const promisifiedFunction = (arg) => {
return new Promise((resolve, reject) => {
callbackFunction(arg, (result) => {
resolve(result);
});
});
};
In the above example, we have a callback-based function called callbackFunction
which takes two arguments: arg
and callback
. The function performs an asynchronous operation and when it's done, it calls the callback
function passing the result
as the argument.
To make the function promise-based, we wrapped it inside a new promise object. Then, we pass the arg
to the callbackFunction
as before, but instead of passing the callback, we use the resolve
function of the promise to return the result.
const promisifiedFunction = (arg) => {
return new Promise(async (resolve, reject) => {
try {
const result = await callbackFunction(arg);
resolve(result);
} catch (error) {
reject(error);
}
});
};
Another option is to use the async/await
syntax to handle the promise. The promisifiedFunction
is now declared as async, and we are using the await
keyword to wait for the callbackFunction
to finish. We're also using a try-catch block to handle potential errors. If everything goes well, the result
will be passed to the resolve
function, otherwise, it will be passed to the reject
function.
Learn more about async/await
in our JavaScript async/await article.
Promise Methods
We already mentioned some of the methods in the examples above. Promise objects in JavaScript come with several built-in methods that allow you to handle different scenarios and situations when working with promises. Here's a list of the most commonly used methods:
// .then() method
promise.then((value) => {
console.log(`Promise resolved with value: ${value}`);
}, (error) => {
console.log(`Promise rejected with reason: ${error}`);
});
// .catch() method
promise.catch((error) => {
console.log(`Promise rejected with reason: ${error}`);
});
//Promise.all() method
Promise.all([promise1, promise2, promise3])
.then((results) => {
console.log(`Promise.all: ${results}`);
})
.catch((error) => {
console.log(`Promise.all: ${error}`);
});
//Promise.race() method
Promise.race([promise1, promise2, promise3])
.then((firstResult) => {
console.log(`Promise.race: ${firstResult}`);
})
.catch((error) => {
console.log(`Promise.race: ${error}`);
});
//Promise.resolve() method
Promise.resolve('Hello World')
.then((result) => {
console.log(`Promise.resolve: ${result}`);
});
//Promise.reject() method
Promise.reject(new Error('Something went wrong'))
.catch((error) => {
console.log(`Promise.reject: ${error}`);
});
//Promise.prototype.finally() method
promise
.then((value) => {
console.log(`Promise resolved with value: ${value}`);
})
.catch((error) => {
console.log(`Promise rejected with reason: ${error}`);
})
.finally(() => {
console.log('Promise settled');
});
Common Uses in Modern Web Applications
Promises are widely used in modern web applications to handle asynchronous operations such as network requests, user input, and timers. Here are a few examples of how promises can be used in web applications:
// Fetching data from an API
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(`Fetched todo: ${data.title}`);
})
.catch((error) => {
console.log(`Error: ${error}`);
});
In the example, we use the Fetch
API which returns a promise that resolves with a Response object. We can then use the .then()
method to handle the response, check if it's ok, and parse the JSON data. If there's an error, it will be caught by the .catch()
method.
// Uploading a file
const input = document.querySelector('input[type="file"]');
const formData = new FormData();
input.addEventListener('change', (event) => {
const file = event.target.files[0];
formData.append('file', file);
fetch('/upload', {
method: 'POST',
body: formData
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(`File uploaded successfully: ${data.fileName}`);
})
.catch((error) => {
console.log(`Error: ${error}`);
});
});
We use the DOM
API to handle a file input and the Fetch
API to upload the file to the server. It listens for a change event on the file input, gets the selected file, appends it to a FormData
object, and sends it to the server via a POST
request. If the server responds with a success status, it logs a success message. If there's an error, it will be caught by the .catch()
method.
Conclusion
Promises provide a more elegant and readable way to handle asynchronous code in JavaScript.
They provide a way to handle errors and allow for promise chaining to handle multiple asynchronous operations.
Promisification can be used to convert callback-based functions to promise-based functions.
The Promise object provides several useful methods to handle promises more efficiently.
The
.then()
method allows you to handle the results of a resolved promise, as well as catch the rejection of a rejected promise.The
.catch()
method is a shorthand for a rejected promise and allows you to handle the rejection of a rejected promise.The
Promise.all()
method allows you to handle the results of multiple resolved promises and the first rejected promise.The
Promise.race()
method allows you to handle the results of the first resolved promise or the first rejected promise.The
Promise.resolve()
method allows you to convert a non-promise value into a resolved promise.The
Promise.reject()
method allows you to create a rejected promise with a specific reason.
Common uses of promises in modern web applications include handling responses from an API, data storage, and user input.
For certain situations, the
async/await
syntax is also a possible option to handle promises.