What is a promise, different states of promises
Promises are objects that represent the eventual completion or failure of an asynchronous operation.
Create a Javascript Promise
A promise in Javascript is exactly what it sounds like – you use it to make a promise to do something, usually asynchronously. When the task completes. you either fulfill your promise or fail to do so.
Promise
is a contructor function, so you need to use the new
keyword to create one. It takes a function, as its argument, with two parameters – resolve
and reject
. These are methods used to determine the outcome of the promise.
The syntax looks like this :
consy myPromise = new Promise((resolve, reject) => {
console.log('inside promise');
});
JavaScriptComplete a Promise with resolve and reject
A promise has three states : pending, fulfilled and rejected
The promise you created in the last challenge is forever stuck in the pending state because you did not add a way to complete the promise. The resolve and reject parameters given to the promise argument are used to do this. resolve is used when you want your promise to succeed, and reject is used when you want it to fail.
These are methods that take an argument, as seen below.
const myPromise = new Promise((resolve, reject) => {
if (condition here) {
resolve("promise was fulfilled");
} else {
reject("promise was rejected");
}
});
JavaScriptThe example above uses strings for the argument of these functions, but it can really be anything. Often it might be an object that you would use data from, to put on your website or elsewhere.
const fetchData = () => {
return new Promise((resolve, reject) => {
// Fetch data from API and resolve reject accordingly
let data = someAPI();
if (data) { // response from server
console.log('we got the data');
resolve(data);
} else {
console.log('data not received');
reject('data fetch error');
}
}
}
JavaScriptHandle a fulfilled promise with then
Promises are most useful when you have a process that takes an unknown amount of time in your code (i.e something asynchronous), often a server request.
When you make a server request it takes some amount of time, and after it completes – you usually want to do something with the response from the server.
This can be acheived by using then
method. The then
method is executed immediately after you promise is filled with resolve.
Here’s an example :
myPromise.then(result => {
// promise is resolved
console.log('do something with the data');
return result;
})
JavaScriptHandle a rejected promise with catch
catch
is the method used when your promise has been rejected. It is executed immediately after a promise’s reject
method is called. Here’s the syntax :
myPromise.catch(error => {
// promise is rejected
console.log('data not received');
throw new error;
});
JavaScriptExample
Imagine a function, createAudioFileAsync()
which asynchronously generates a sound file given a configuration record and two callback functions, one called if the audio file is successfully created, and the other called if an error occures.
function successCallback(result) {
console.log(`Audio file ready at URL: ${result}`);
}
function failureCallback(error) {
console.error(`Error generating audio file: ${error}`);
}
createAudioFileAsync(audioSettings, successCallback, failureCallback);
JavaScriptIf createdAudioFileAsync()
were rewritten to return a promise, you would attach your callbacks to it instead:
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
JavaScriptThis convention has several advantages. We will explore each one.
Promise Chaining
With promises, we can now execute two or more asynchronouse operations back to back, where each subsequent operation starts when the previous operation succeeds, with the result from the previous step.
Earlier, before promises, doing several asynchronous operations in a row lead to the classic Callback Hell
What is “callback hell” ?
Asynchronous Javascript, or javascript that uses callbacks, is hard to get right.. A lot of code ends up looking like this:
doSomething(function (result) {
doSomethingElse(result, function (newResult) {
doThirdThing(newResult, function (finalResult)
{
console.log(`Got the final results: ${finalResult}`);
}
}, failureCallbacl);
}, failureCallback);
JavaScriptThe cause of callback hell is when people try to write Javascript in a way where execution happens visually from top to bottom.
Lots of people make this mistake! In other languages like C, Ruby or Python there is the expectation that whatever happens on line 1 will finish before the code on line 2 starts running and so on down the file. As you will learn , Javascript is different
With Promise Chaining we can create longer chains of processing, where each promise represents the completion of one asynchronous step in the chain
// Promise Chaining
doSomething()
.then(function (result) {
return doSomethingElse(result);
})
.then(function (newResult) {
return doThirdThing(newResult);
})
.then(function (finalResult) {
console.log(`Got the final result ${finalResult}`);
})
.catch(failureCallback);
JavaScriptYou might see this expressed with arrow functions instead :
// Promise chaining with arrow functions
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log(`Got the final results ${finalResult}`);
})
.catch(failureCallback);
JavaScriptAvoid floating promises
It is important to always returen promises from then callbacks, even if the promise always resolves to undefined.
If the previous handler started a promise but did not return it, there’s no track its settlement anymore, and the promise is said to be “floating”.
doSomething()
.then((url) => {
// Missing `return` keyword in front of fetch(url).
fetch(url);
})
.then((result) => {
// result is undefined, because nothing is returned from the previous handler
// There's no way to know the return value of the fetch() call anymore, or whether it succeeded at all
});
JavaScriptBy returning the result of the fetch call (which is a promise), we can both track its completion and receive its value when it completes.
doSomething()
.then((url) => {
return fetch(url); // return keyword added
})
.then((result) => {
// result is a Response object
});
JavaScriptFloating promises could be worse, if you have race conditions – If the promise from the last handler is not returned, the next then
handler will be called early, and any value it reads may be incomplete.
const listOfIngredients = [];
doSomething()
.then((url) => {
// Missing `return` keyword infront of fetch(url).
fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
})
.then(() => {
console.log(listOfIngredients);
// listOfIngredients will always be [], because the fetch request hasn't completed yet
});
JavaScriptTherefore, as a rule of thumb, whenever your operation encounters a promise, return it and defer its handling to the next then
handler.
const listOfIngredients = [];
doSomething()
.then((url) => {
// `return` keyword now included in front of fetch call.
return fetch(url)
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
});
})
.then(() => {
console.log(listOfIngredients);
// listOfIngredients will now contain data from fetch call.
});
JavaScriptEven better, you can flatten the nested chain into a single chain, which is simpler and makes error handling easier.
doSomething()
.then((url) => fetch(url))
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
})
.then(() => {
console.log(listOfIngredients);
});
JavaScriptComing soon :
- Using async/await
- Chaining after a catch
- Promise rejection events
- Promise Composition (Promise.all(), allSettled(), any(), race()
- Promise Cancellation
- Task queues vs microtasks
- Error handling