Skip to main content

Promises

Poly promises to bake a cake for my birthday in two weeks. If all goes well and she does not get sick, I will have a cake. If Poly does not feel well, she will not be able to bake the cake for me. A promise is not a guarantee of fulfillment, we do not know its outcome in advance. In programming, there are also tasks with the outcome to be known only in the future.

Promise story about cake

Promise is an object representing the current state of an asynchronous operation. This is a wrapper for a value unknown at the time the promise is created. It makes it possible to handle the results of asynchronous operations as if they were synchronous: instead of the result of an asynchronous operation, a promise of the future result is returned.

Promises can assume three states:

  • Pending: initial state when creating a promise.
  • Fulfilled: the operation was completed successfully, with some result.
  • Rejected: the operation was rejected with an error.
Promise states

When a promise is created, it is pending, after which it can be settled successfully (fulfilled), returning a result (value), or with an error (rejected), returning a reason. When a promise enters the fulfilled or rejected state, this is forever.

Note

When a promise is fulfilled or rejected, it is said to be settled. It is simply a term to describe that a promise is in any state other than pending.

Differences between promises and callbacks:

  • Callbacks are functions, promises are objects.
  • Callbacks are passed as arguments from outer to inner code, whereas promises are returned from inner to outer code.
  • Callbacks handle successful or unsuccessful completion of an operation, while promises do not handle anything.
  • Callbacks can handle multiple events, and promises are associated with only one event.

Creation

A promise is created as an instance of the Promise class, which takes a function (executor) as an argument and calls it immediately, even before the promise is created and returned.

const promise = new Promise((resolve, reject) => {
// Asynchronous operation
});

The executor function notifies the instance (promise) when and how the related operation is going to complete. In it, you can perform any asynchronous operation; upon completion, you need to call either resolve() on success (fulfilled state), or reject() on error (rejected state). The return value of this function is ignored.

  • resolve(value) is a function to be called upon successful operation. The value of the fulfilled promise will be passed to it as an argument.
  • reject(error) is a function to be called in case of an error. The value of the rejected promise will be passed to it as an argument.
Creating promise
// Change value of isSuccess variable to call resolve or reject
const isSuccess = true;

const promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (isSuccess) {
resolve("Success! Value passed to resolve function");
} else {
reject("Error! Error passed to reject function");
}
}, 2000);
});

A promise (object) in the pending state will be written to the variable promise, and two seconds later, after resolve() or reject() is called, the promise will change its state to fulfilled or rejected, and you will be able to handle it.

then() method

Code that needs to do something asynchronously creates a promise and returns it. The outer code, having received a promise, hangs up handlers on it. When the process is completed, the asynchronous code makes the promise go to the fulfilled or rejected state, after which handlers in the outer code are automatically called.

After the promise is created, its result is handled in callback functions. Code is written in terms of what might happen if the promise is fulfilled or not, without considering the time frame.

then() takes two arguments: callbacks that will be called when the promise changes its state. They will receive the result of the Promise, a value or error, as arguments.

promise.then(onResolve, onReject)
  • onResolve(value) will be called when the promise is successful and will receive its result as an argument.
  • onReject(error) will be called when the promise throws an error and will receive it as an argument.
Method then

In the example, the onResolve callback will be called two seconds later if the promise is fulfilled, while onReject will be called two seconds later if the promise is rejected.

// Change value of isSuccess variable to call resolve or reject
const isSuccess = true;

const promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (isSuccess) {
resolve("Success! Value passed to resolve function");
} else {
reject("Error! Error passed to reject function");
}
}, 2000);
});

// Will run first
console.log("Before promise.then()");

// Registering promise callbacks
promise.then(
// onResolve will run third or not at all
value => {
console.log("onResolve call inside promise.then()");
console.log(value); // "Success! Value passed to resolve function"
},
// onReject will run third or not at all
error => {
console.log("onReject call inside promise.then()");
console.log(error); // "Error! Error passed to reject function"
}
);

// Will run second
console.log("After promise.then()");
Note

When the onResolve and onReject functions’ logic is complex , for convenience they are declared as external functions and passed to the then() method by name.

catch() method

In practice, then() is only used to handle fulfilled promises, while execution errors and handled in a special catch() method to "catch" errors.

Method catch
promise.catch(error => {
// Promise rejected
});

The callback will be called when the promise throws an error and will receive it as an argument.

// Change value of isSuccess variable to call resolve or reject
const isSuccess = true;

const promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (isSuccess) {
resolve("Success! Value passed to resolve function");
} else {
reject("Error! Error passed to reject function");
}
}, 2000);
});

promise
.then(value => {
console.log(value);
})
.catch(error => {
console.log(error);
});

finally() method

This method can be useful if you need to execute code after the promise is fulfilled or rejected, regardless of the outcome. It prevents code duplication in the then() and catch() handlers.

Method finally
promise.finally(() => {
// Promise fulfilled or rejected
});

The callback will not receive any arguments because the promise status, fulfilled or rejected, cannot be checked. Here only the code will be executed that must be run in any case.

// Change value of isSuccess variable to call resolve or reject
const isSuccess = true;

const promise = new Promise((resolve, reject) => {
setTimeout(() => {
if (isSuccess) {
resolve("Success! Value passed to resolve function");
} else {
reject("Error! Error passed to reject function");
}
}, 2000);
});

promise
.then(value => console.log(value)) // "Success! Value passed to resolve function"
.catch(error => console.log(error)) // "Error! Error passed to reject function"
.finally(() => console.log("Promise settled")); // "Promise settled"

Promises chaining

then() returns another promise with a value returned by its onResolve callback. This enables you to build asynchronous chains from promises.

Promise chain

Since the then() method returns a promise, it may take some time before it is executed, so the rest of the chain will wait. If an error occurs anywhere in the chain, execution of all subsequent then() is canceled, and control is passed to the catch() method. Which is why it belongs to the end of the promises chain.

const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(5);
}, 2000);
});

promise
.then(value => {
console.log(value); // 5
return value * 2;
})
.then(value => {
console.log(value); // 10
return value * 3;
})
.then(value => {
console.log(value); // 30
})
.catch(error => {
console.log(error);
})
.finally(() => {
console.log("Final task");
});

Promisification of functions

Let's imagine that you have an asynchronous function that performs an asynchronous operation, for example, a request to the server. In order to handle the result, it should be able to wait for two callbacks, for a successful request and for an error.

const fetchUserFromServer = (username, onSuccess, onError) => {
console.log(`Fetching data for ${username}`);

setTimeout(() => {
// Change value of isSuccess variable to simulate request status
const isSuccess = true;

if (isSuccess) {
onSuccess("success value");
} else {
onError("error");
}
}, 2000);
};

const onFetchSuccess = user => {
console.log(user);
};

const onFetchError = error => {
console.error(error);
};

fetchUserFromServer("Mango", onFetchSuccess, onFetchError);

Now the fetchUserFromServer() function knows too much about the code that will use its outcome. It expects callbacks and is responsible for calling them under certain conditions. That is, you pass something inside the function (callbacks) and hope that it will work correctly, which is not good.

It is better when the function does not care about the code that will use its result. It just performs an operation and returns its result to outer code. In order to return the result of an asynchronous operation, a promise must be returned from the function. Promisification means transforming a function with callbacks so that it does not accept callbacks, but returns a promise.

const fetchUserFromServer = username => {
return new Promise((resolve, reject) => {
console.log(`Fetching data for ${username}`);

setTimeout(() => {
// Change value of isSuccess variable to simulate request status
const isSuccess = true;

if (isSuccess) {
resolve("success value");
} else {
reject("error");
}
}, 2000);
});
};

fetchUserFromServer("Mango")
.then(user => console.log(user))
.catch(error => console.error(error));
Note

Most modern libraries are promise-based. When a method is called for an asynchronous operation, its result is available as a promise, to which you can add handlers in the then() and catch() methods.