In this article, we are going to discuss Javascript promises and how they are going to solve problems created by traditional callback functions.
this article is part of larger series Asynchronous Programming In Javascript. so if you are not following please do check it out.
In a previous article we looked at the following problems with traditional callback functions.
- Error Handling.
- different order of execution for synchronous and asynchronous functions.
- lack of control over the code.
- jumping from one place to another for debugging the code.
- Before I show you the code about the promise and its pattern I am going to give you the philosophy behind the promise so that you can understand the code better.
- let's say you are at the RTO(DMV for the western audience) for your driving license. at the counter, you submit your document and they give you the token number. the token number is not the license but it is a promise that when that number comes on the screen you should get your driving license. meanwhile, you can read a book or binge-watch tv show😁 .
eventually, you hear your token number and you give your receipt to the employee at the RTO(or DMV) and you get your license. In other words, once your value was ready, you exchanged your value-promise for the value itself.
but there can be also another outcome, let's say there is a problem in the server and they no longer can provide the license to the customer for a week. in that case, you are rejected from the license.
Promise API:-
-keeping the above analogy in mind you can say the Promise can be fulfilled
(we get the license) or it can be rejected
(we don't get our license).
const dataFromServer = new Promise(function(resolve,reject){
// does long process e.g. fetching data from server
// and calls resovle(...) or reject(...) which are resolution callback for promise
});
As shown in the code above
Promise
takes a function with Parameterresolve
andreject
.These are the resolution functions for the promise.resolve(..)
generally signals fulfillment, andreject(..)
signals rejection.dataFromServer
is our promise which can befulfilled
( call with resolve(..) ) orrejected
. we can call the.then
method on the promise which takes two callback functions, first callback handles what happens if we invokeresolve(..)
in the promise creation and the second callback function handles what happens if we invokereject(..)
or there is an error in promise creation. which would look something like this.
const dataFromServer = new Promise(function(resolve,reject){
// does long process e.g. fetching data from server
// and calls resovle(...) or reject(...) which are resolution callback for promise
});
dataFromServer.then(function onFulfillment(){
// this function is called when resolve() is called
},
function onReject(){
// this function is called when reject() is called
});
- If Some error occurs while creating the promise, for example, we call the function that does not exist in promise creation it will call the
onReject
in thethen
function.
const dataFromServer = new Promise(function(resolve,reject){
imaginaryFunction(..); // throws the error
// and calls resovle(...) or reject(...) which are resolution callback for promise
});
dataFromServer.then(function onFulfillment(){
// this function is called when resolve() is called
},
function onReject(){
// this function is called when reject() is called or there is an error in promise creation.
});
- the picture from MDN docs gives us a better understanding the flow of javascript promise.
- if you don't understand that
catch(..)
don't worry about it. we will cover this later in the post.
is Promise synchronous or asynchronous or both ???
- In the callback functions we were not sure about their nature. but thankfully in promise, it always runs asynchronously. so we can get the expected behavior no matter what the underlying code is doing. let's look at the example.
const x = Promise.resolve(23);
x.then((v)=>{
console.log(v)
});
console.log("b")
// prints b 23
- Here
promise x
has a code that runs synchronously but the output shows that it runs asynchronously. because Promise always runs asynchronously. that is good news we can finally get the expected behavior every time we run promise.
-this is a very deep topic. so if you want to dive deeper please check this blog post.
Promise chain
Promises are not just a mechanism for a single-step this-then-that sort of operation. That's the building block, of course, but it turns out we chain multiple Promises together to represent a sequence of async steps.
Every time you call
then(..)
on a Promise, it creates and returns a new Promise, which we can chain with.- Whatever value you return from the
then(..)
call's fulfillment callback (the first parameter) is automatically set as the fulfillment of the chained Promise.
const p = Promise.resovle("dog");
const p2 = p.then(function(v){
return v.toUpperCase();
})
p2.then(function(val){
console.log(val) // "DOG"
});
- In this example value returned from fulfillment callback of
p.then(..)
("DOG")fulfills
the P2 promise. so whenever we call thefulfillment
callback ofp2.then(..)
it gets the value returned fromp.then(...)
as an argument.
- whenever you want to manipulate the array with a
filter
, themap
function you can do something like this
// Here ans is the array that has double of given value > 5, for the given array
const x = [1,2,3,4];
const ans = x.map((val)=>val * 2).filter(val => val > 5);
- here instead of creating an intermediate array that has double of the given value we are chaining the array functions. same thing we can do that with our
promise
example shown above. - so let's chain that promise.
const p = Promise.resovle("dog");
p.then(function(v){
return v.toUpperCase();
}).then(function(val){
console.log(val) // "DOG"
});
- that looks great and we don't need to create a temp variable, but what if first
then(...)
returnspromise
instead of normal javascript type. - Well in that case it unwraps the promise and passes the value which we have passed in
resolve
const p = Promise.resovle("dog");
p.then(function(v){
return new Promise(function(resolve,reject){
resolve(v.toUpperCase());
})
}).then(function(val){
console.log(val) // "DOG"
});
Here we are returning the promise which
resolve
to thev.toUpperCase()
, butval
in the second promise chain does not get thepromise
but gets theresolve
value of the returned promise. that is awesome. because of this kind of behavior, we are confident that we always get thenormal
value notpromise
.The key to making a Promise sequence truly async capable is that whenever there is Asynchronous code that takes time in the chain it should wait for that code to finish before continuing that chain.
const p = Promise.resovle("dog");
p.then(function(v){
return new Promise(function(resolve,reject){
setTimeout(function (){
resolve(v.toUpperCase());
},1000)
})
}).then(function(val){
// this code runs after 1000 ms delay in previous step .
console.log(val) // "DOG"
});
That's awesome! Now we can create a chain of however many async steps we want, and each chain can delay the next step, as necessary.
with Promise chaining we don't need to jump from one function to another like a callback function instead every function is in the chain, so it is easier to maintain.
Error Handling in Promise :
In the above examples you have already seen that the second argument of the
then
function is called whenever thereject
function is called in the above promise chain.now let's look at the example where there is an error in the
then
chain or in the promise creation and how we can handle that error.
const p = new Promise(function (resolve,reject){
resolve(23)
})
p.then(function(v){
return v.toUpperCase() // thorws error.
},function onReject(){
// this function does not get called when there is an error in first callback.
}).then(null,function(err){
// this one called when we execute the v.toUpperCase()
console.error(err)
})
When We encountered an error in the
fulfillment
callback in the first chain it does not call the function provided in the second argument because the second argument is called when the previous promise in the chain arerejected
or haveerror
but those promise in chains are alreadyfulfilled
so when an error in the first callback occurs it finds the next reject handler and calls that function.Having two callback functions in the
then
block does not look good in terms of readability so javascript has new building block calledcatch
..catch
is just a.then(null,function(){})
. so it can provide great flexibility in our program and we can separate thefulfillment
code handler andreject
orerror
code handler.
BEFORE
const p = Promise.resovle([]);
p.then(function(v){
return new Promise(function(resolve,reject){
resolve(v.toUpperCase()); // will throw the error
})
})
.then(function(val){
console.log(val) // "DOG"
}, function(err){
// will handle the error
console.error(err)
})
AFTER
const p = Promise.resovle([]);
p.then(function(v){
return new Promise(function(resolve,reject){
resolve(v.toUpperCase()); // will throw the error
}
})
.catch(function(err){
// with catch block we can catch the error with more readability.
console.error(err)
})
.then(function(val){
console.log(val) // "DOG"
})
- You can see that with a catch block we can easily spot the code that handles error for the promises in the chain above the
catch
block. - you can insert a
catch
block anywhere and it will handle the error for promises in the chain above thecatch
block.
gaining the trust and control back with a promise :
let's review the trust issue with the
callback
function.calling the callback function too early.
- calling the callback function too late.
- forget to call the callback function.
calling the callback more than necessary.
calling callback too early or too late:
traditional callback function may run asynchronously or synchronously so the
callback
function may be called too early if we expect them to call asynchronously and it may get called too late if we expected them to call synchronously.On the other hand
promise
always runs asynchronously so we always get the same behavior in every scenario.
forget to call the callback function.
- if you register
fulfillment
andrejection
callback to promise, nothing can prevent the promise from notifying you of its resolution. - but what if the promise never gets resolved in either way? even in that scenario, we can use a prototype method call
Promise.race
. Promise.race
takes an array of promises and fulfills or rejects as soon as one of the promises in an array fulfills or rejects.
function waitForTime(ms) {
return new Promise((resolve,reject) => setTimeout(reject(throw Error(`request did not resolve in ${ms} milisecond`)) , ms));
}
Promise.race( [
fetch("www.fetchStuff.com"),
waitForTime( 5000 ) // give it 5 seconds
] )
.then((val)=>{
console.log(val)
})
.catch(err =>{
console.log("err") // will throw error if fetch request does not resolve in 5 seconds.
});
- in this example
Promise.race
has two promises. first request data from the server and the other one just wait for 5 seconds and rejects promise. if we fetch request promise resolves before 5 seconds second won't get fired and everything works perfect but if fetch promise does not resolve in 5 seconds it willthrow
error sayingrequest did not resolve in 5000 milliseconds
. so in general if our promise does not resolve in some time we will throw the error.
- in general, we use a library like
Axios
.Axios
has the property nametimeout
in the config object. which we can use to achieve the same behavior as above.
axios.post("www.fetchStuff.com",{data},{timeout:5000})
.then((val)=>{
console.log(val)
})
.catch(err =>{
console.log("err") // will throw error if fetch request does not resolve in 5 seconds.
});
- this code will do the same as the code with
Promise.race
.
calling callback function too many times.
In the case of callback function if call the function too many times it may cause unexpected behavior. for example, if you call the callback function multiple times that is responsible for credit card transactions you will charge the customer multiple times for the item and there is nothing you can about it because API is responsible for calling the
callback
function.Promises are defined so that they can only be resolved once. If for some reason the Promise creation code tries to call
resolve(..)
orreject(..)
more than once. the Promise will accept only the first resolution, and will silently ignore other attempts.because promise resolves only once, the promise-related callback in
.then( )
only gets called once.
- that's it, folks. I hope you all learn something from the article and if you have any query or feedback please share thoughts in the comment section. In the next article, we will talk about javascript iterator and generator and how they responsible for modern
async-await
syntax.