We discussed about callback functions in our previous guide: AJAX (Asynchronous JavaScript and XML) and Using The XMLHttpRequest API.
In this guide, we would look at promises in JavaScript, which is also similar to a callback function but much more robust and intuitive to use.
To better understand promises, you might wanna take a refresher on callback, this would let you understand the use of promises.
- So, what is a callback?
- Uses of Promises
- Using a Promise
- Chaining
So, what is a callback?
A callback is a function or a set of code that is passed as an argument to another code or function, the other code you are passing the callBack to is expected to call back (execute) the function you passed at a given time. It can either be synchronous (immediately) or asynchronous (later in time).
An example of a callback:
function greeting(firstName, callback) {
firstName = firstName.toUpperCase()
return callback(firstName);
}
function getUserInfo(firstName) {
return firstName;
}
console.log(greeting('the_devsrealm_guy', getUserInfo));
// Output => THE_DEVSREALM_GUY
I have explained the above called in the guide I linked to above, but here is a summary:
Think of the getUserInfo function as a function that only gets the user info, and you can think of the greeting function as a function that collects that info and does some operation on it, and once it does it, it returns the result to the callback function.
So, a callback is a function that is passed as an argument to another function, to be “called back” at a later time, so, the callback is actually waiting for the greeting function to perform its operation and give it the data.
When you start writing an asynchronous callback, things start to get a bit interesting:
Let’s see a comparison of an actual synchronous code and then we do an asynchronous callback:
For Synchronous:
// Synchronous
const posts = [
{title: 'Post One', body: 'This is post one'},
{title: 'Post Two', body: 'This is post two'}
];
function addPost(post) {
// Delay for 3 seconds
setTimeout(function() {
posts.push(post); // Add new object on to the end of the array
}, 3000);
}
function getPosts() {
setTimeout(function() {
posts.forEach(function(post){
console.log(`${post.title}`);
});
}, 1000);
}
addPost({title: 'Post Three', body: 'This is post three'});
getPosts();
Your output would be:
Post One
Post Two
The third post didn't get added because the code is synchronous. The addPost function got delayed by three seconds, so, before it could catch up, the getPosts() function has been called. The getPosts() function was delayed for a second, but it doesn't really make any sense, since it would only be called after a second while addPost function still has 2 seconds before it would ever get processed.
This is where asynchronous callback comes in…So…
For Asynchronous:
// Asynchronous Callback
const posts = [
{title: 'Post One', body: 'This is post one'},
{title: 'Post Two', body: 'This is post two'}
];
function addPost(post, callback) {
// Delay for 3 seconds
setTimeout(function() {
posts.push(post);
callback(); // callback the getPosts function
}, 3000);
}
function getPosts() {
// Delay for a second
setTimeout(function() {
posts.forEach(function(post){
console.log(post.title);
});
}, 1000);
}
addPost({title: 'Post Three', body: 'This is post three'}, getPosts);
For sure, the output is:
Post One
Post Two
Post Three
The addPost function takes in two arguments, the first one is the post object, and the second one is the callback. When the addPost is called, it would add the post to the array, and execute the callback function before it times out.
Now, if you can do all this with a callback, what is the use of Promises...
Uses of Promises
First, Promise is introduced to deal with asynchronous code, and they are not that different from an asynchronous callback.
In short, a Promise is a callback if we take it by the definition of a callback: a function that is passed as an argument to another function, to be “called back” at a later time, which is what Promises is all about, the good thing about using Promises instead of the old callback method is that:
- Promises are always asynchronous, unlike a regular callback function that can either be sync or async, so, whenever you are doing say A(Async)JAX call, you would most likely be using Promise since they are built for that.
- Promises are good for a nested callback; this would make your code readable. An example is if you call an action in succession, e.g. a callback A, B, and C. This can be easily implemented in Promise, whereas in a regular callback function, you would have to force the order (A then callback B then callback C), which can be confusing, and unreadable.
- Promise offers a better way to handle errors in which you can handle them at any given stage by calling catch(), it will catch any error that might occur anywhere before it, this is also possible with a regular callback, but you would have to build the logic yourself.
- When using Promises, a callback added with then(), will be called even after the success or failure of the asynchronous operation.
A promise can really be robust, and easier to use than the regular callback function...
Using a Promise
Now, let's convert the following regular callback:
// Asynchronous Callback
const posts = [
{title: 'Post One', body: 'This is post one'},
{title: 'Post Two', body: 'This is post two'}
];
function addPost(post, callback) {
// Delay for 3 seconds
setTimeout(function() {
posts.push(post);
callback(); // callback the getPosts function
}, 3000);
}
function getPosts() {
// Delay for a second
setTimeout(function() {
posts.forEach(function(post){
console.log(post.title);
});
}, 1000);
}
addPost({title: 'Post Three', body: 'This is post three'}, getPosts);
To a promise:
// Asynchronous - Promises
const posts = [
{title: 'Post One', body: 'This is post one'},
{title: 'Post Two', body: 'This is post two'}
];
function addPost(post) {
return new Promise(function (resolve, reject) {
// Delay for 3 seconds
setTimeout(function() {
posts.push(post);
resolve();
}, 3000);
})
}
function getPosts() {
// Delay for a second
setTimeout(function() {
posts.forEach(function(post){
console.log(post.title);
});
}, 1000);
}
addPost({title: 'Post Three', body: 'This is post three'}).then(getPosts);
In the addpost function, we return a promise object which has a function that takes in two arguments, resolve and reject.
- In the addPost function, I removed the callback argument since I'll be using Promise anyway.
- after the
posts.push(post);
I added resolve(), meaning as soon the post object is pushed to the posts array we can call our resolve method - When calling the addPost:
addPost({title: 'Post Three', body: 'This is post three'}).then(getPosts);
we then added a .then(getPosts) which means if the promise resolves, that is if the promise is successful, we run the getPosts function, isn't this super simple?
We are only passing in a resolve(), what if our promise fails, or let me say, what if we do not get the value we are expecting, in that case, we use the reject method to catch that in time, here is an example:
// Asynchronous - Promises
const posts = [
{title: 'Post One', body: 'This is post one'},
{title: 'Post Two', body: 'This is post two'}
];
function addPost(post) {
return new Promise(function (resolve, reject) {
// Delay for 3 seconds
setTimeout(function() {
posts.push(post);
let test = false;
(test) ? resolve() : reject("Couldn't add a post"); // if test is true - this is false cos we set it to false
}, 3000);
})
}
function getPosts() {
// Delay for a second
setTimeout(function() {
posts.forEach(function(post){
console.log(post.title);
});
}, 1000);
}
addPost({title: 'Post Three', body: 'This is post three'})
.then(getPosts)
.catch(function (err) {
console.log(err)
});
Well, you can't just add a reject() in the hope that Promise is gonna catch it by itself, you still have to build logic around that, so, in the above example:
- I purposely define a "test" variable and set it to false
- and here:
(test) ? resolve() : reject("Couldn't add a post");
I said if the test is true, resolve, and if false reject, it would call reject or a failed promise because I have set the test variable to false - You then catch the error:
.catch(function (err) {
console.log(err)
});
It can't get simpler than that. Note, you don't have to call it resolve and reject here: return new Promise(function (resolve, reject)
you can call it whatever you like, just note that the first one stands for fulfilled and the second one handles any failed error.
Here is the general idea or structure of a promise:
let something = new Promise(function(resolve, reject) {
// do a thing, could be an async..who cares
if (/* everything is succesfull */) {
resolve("Success, you don't have to add a string like I am doing though");
}
else {
reject(Error("It failed"));
}
});
You then use the promise like so:
something.then(function(result) {
console.log(result); // "Success, you don't have to add a string like I am doing though"
}, function(err) {
console.log(err); // Error: "It failed"
});
.then() takes two arguments, a callback for a success case, and another for the failure case. Both are optional, so you can add a callback for the success or failure case only.
Chaining
I forgot to mention this, well, I am doing so now ;)
You can chain multiple .then() methods together, and they’ll run in succession. Here is an example:
let something = new Promise(function(resolve, reject) {
resolve(1);
});
something.then(function(val) {
console.log(val); // 1
return val + 1;
})
.then(function(val) {
console.log(val); // 2
return val + 1;
})
.then(function(val) {
console.log(val); // 3
});
// => Output
1
2
3
It will resolve(1) immediately since we aren't really doing anything other than passing 1 as an argument. It would then pass along 1 as an argument.
As you can see, I chained a couple of .then() methods together. The first one logs the val, increases it by 1 and returns it to the next argument in the sequence, and so on.
This would conclude my guide on Promises, there is more you can learn about promises, but this is sufficient for a start.