Introduction

In JavaScript, there is a term for a piece of code with nested callback functions called “callback hell” or “pyramid of doom”. Debugging such program code and even just understanding it is very difficult. With the ES2015 standard came a new syntax called “promise” (Serb. promise), which with its API provides a better and clearer way to organize callback functions. This is especially noticeable when working with asynchronous operations because with promises the syntax is very similar to standard synchronous syntax.
Example – “callback hell”
The most well-known and most common application of callback functions is “handling” of actions after an event has occurred. It is the “handling” of interdependent events that is most often the cause of “callback hell”. This example shows the appearance of such code:
|
1 2 3 4 5 6 7 8 9 |
callEndpoint("api/getidbyusername/nekoime", function (result) { callEndpoint("api/getifolowersbyid/" + result.userID, function (result) { callEndpoint("api/nekidrugizahtev/" result.folowers, function (result) { callEndpoint("api/nekidrugizahtev/" result.folowers, function (result) { // uhh it's already chaos here! }); }); }); }); |
What is a Promise?
A
Promise is a javascript object that represents a “placeholder” for the results of an asynchronous function as long as the execution of the asynchronous operation lasts.
A promise is a synchronously returned object during an asynchronous operation, which is a temporary replacement for the possible results of that asynchronous operation.
Promise instead of an ultimate value, makes a promise to deliver that value at some point in the future. Since promise objects are a temporary replacement for a future eventual value, this allows us to hook handlers for the future result of an asynchronous operation through it. With this new capability, we have almost equalized asynchronous operations with synchronous ones. Now both synchronous and asynchronous operations can return some value, as synchronous ones immediately return the final data and asynchronous “placeholders” for future data.

An asynchronous function can have two possible end results, which are “successfully executed operation” or “unsuccessfully executed operation”, while promise can be in one of three states:
- Pending – when the asynchronous action is still executing
- Fulfill – when the asynchronous action is completed successfully
- Reject – when an asynchronous operation failed with an error
The whole promise mechanism can be divided into two parts:
- Creating promises inside an asynchronous function
- Using the created promise (the code is outside the async function)
Promises creation
To replace the future end result of an asynchronous function with a promise object, the function needs to return a new promise object through its code:
|
1 2 3 |
function asinhronaFunkcija() { return new Promise(function (){...}); } |
A function (so-called executor function) is passed to each new promise as a parameter, which processes the asynchronous operation itself and the future results of that asynchronous operation. What is interesting to us is the part where it processes the possible results of an asynchronous operation, when depending on the success of the operation, it calls one of the two functions passed to it as parameters:
- Function resolve() is called in the part of the code that processes a successfully completed asynchronous operation. The parameter of this function represents the obtained data from a successfully completed operation, therefore the resolve() function is used to pass the resulting data to the corresponding “handler” method through its parameter, e.g. then() or Promise.all()…
- The function “reject()” is called in the part of the code that handles the case when there is a problem with the execution of an asynchronous operation. Through its parameter, it passes the reason for the failure of the asynchronous operation to the corresponding “handler”, most often the catch() method.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function asinhronaFunkcija() { return new Promise(function (resolve, reject) { //...kod za asinhronu operaciju... if (successful operacija) { resolve(result_value); } else { reject(error); } } ); } |
Use of promises
Using promis means processing any results obtained after the completion of an asynchronous operation.
Promise.prototype.then()
Promise reacts to a change in its state by calling the callback function. If the promise is in the fullfiled state after the state change, the then() method is called. This method accepts two parameters “onFulfilled” and “onRejected” which are of type function.
|
1 |
Promise.prototype.then(onFulfilled(), onRejected()) |
The first function onFulfilled “handles” a successfully completed asynchronous operation. It accepts one parameter, and through it the data obtained by an asynchronous operation is passed to it. While the second function “onRejected” “handles” the unsuccessfully completed asynchronous operation and also accepts one parameter through which the reason for the failure is passed to it.
|
1 2 |
nekiPromise.then(function(podatak) { // part of the code when the asynchronous operation is successful }, function(razlog) { // part of the code when the asynchronous operation failed }); |
Example
In this example, through the resolve() function parameter, we pass data (xhr.response) to the onFulfilled() function, while through the reject() function parameter, we pass the error type “new Error(xhr.statusText)” to the onRejected() function.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function makeRequest (method, url) { return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open(method, url); xhr.onload = function() { if (xhr.status === 200) { resolve(xhr.response); } else { reject(new Error(xhr.statusText)); } }; xhr.onerror = () => reject(new Error("Network error")); xhr.send(); }); } // Part II - Beyond the Asynchronous Function: makeRequest('GET', 'http://example.com') .then( function(data){console.log(data);}, function(err){console.error(err);} ) |
Promise.prototype.catch()
If after the state change, the promise is in the rejected state, the catch() method is called.
|
1 |
Promise.prototype.catch(onRejected()) |
The catch() method accepts a callback function (called “onRejected()”) as a parameter, which is responsible for accepting and processing the error.
EXPLANATION:
Although the then() method can “handle” past successful results and unsuccessful ones, it is not used that often. Most often, the then() method is used to “handle” only a successful result (only the first parameter is used), while the catch() method is used in case of an unsuccessful operation.
|
1 |
asinhronaFunkcija().then(result_value => { ··· }).catch(error => { ··· }); |
Or written a little more clearly:
|
1 2 3 |
asinhronaFunkcija() .then(result => { ··· }) .catch(error => { ··· }); |
Example
The example shows the handling of ajax asynchronous operation with a promise that uses then() and catch() methods, but also with “outdated” callback functions:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function makeRequest (method, url) { return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open(method, url); xhr.onload = function() { if (xhr.status === 200) { resolve(xhr.response); } else { reject(new Error(xhr.statusText)); } }; xhr.onerror = () => reject(new Error("Network error")); xhr.send(); }); } // Part II - Beyond the Asynchronous Function: makeRequest('GET', 'http://example.com') .then(function (data) { console.log(data); }) .catch(function (err) { console.error('Uhh imamo problem!', err); }); |
In this example, “xhr.responseText” is passed to “data” via the function parameter resolve()parameter of the then() method, and via the parameter of the reject() function, “new Error(“Network error”)” is passed to “err”. parameter of the catch() method
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function makeRequest (method, url, callbackFunkcija) { var xhr = new XMLHttpRequest(); xhr.open(method, url); xhr.onload = function () { callbackFunkcija(null, xhr.response); // null is passed instead of errr }; xhr.onerror = function () { callbackFunkcija(error); }; xhr.send(); } makeRequest('GET', 'http://example.com', function (error, data) { if (error) { throw error; } console.log(data); }); |
Chaining of promises
Since the then() and catch() methods always return a new promise, they can be chained. Using this feature opens the door to elegantly solve the problem of many chained events calling callback functions, commonly known as “callback hell”.
Example
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
const promise = new Promise( function(resolve, reject) { setTimeout( () => { resolve( [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] ); }, 2000); }); function ispisiRezultat(value) { console.log(value); return value; } function samoParni(array) { return array.filter( (value) => { return (value % 2) === 0; }); } function sumaClanovaNiza(array) { return array.reduce( (a, b) => { return a + b; }, 0); } function errorHandler(err) { console.log("ERROR"); console.log(err); } promise .then(ispisiRezultat) // Returns: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] .then(samoParni) .then(ispisiRezultat) // Returns: [0, 2, 4, 6, 8] .then(sumaClanovaNiza) .then(ispisiRezultat) // Returns: 20 .catch(errorHandler); |
Promise.resolve()
Promise.resolve(value) is a method that creates promises of different types. It can take three different types as a parameter, after which it returns promise:
- Promise.resolve(value)
When a value is passed to this method, that value will be passed to the then() function through its parameter. - Promise.resolve(otherPromise)
When another promise is passed, the eventual state of the accepted promise object is passed through the parameter of the then() method. This is the most common case, as it is used to convert promises created by other libraries.
Example
12345678910var pocetniPromise = Promise.resolve(33);var drugiPromise = Promise.resolve(pocetniPromise);drugiPromise.then(function(value) {console.log('value: ' + value);});console.log('pocetniPromise === drugiPromise je ' + (pocetniPromise === drugiPromise));// Output to the console (pay attention to the printing order!):// "pocetniPromise === drugiPromise je true"// value: 33Note the order of printing in the console, the order is different than in the code, because the “then handlers” are called asynchronously. Read more about this in the article Basics of asynchronous programming in JavaScript
- Promise.resolve(thenableObject)
This method can accept as a parameter the so-called “thenable object”:
Example
12345678910var p1 = Promise.resolve({then: function(onFulfill, onReject) { onFulfill('fulfilled!'); }});console.log(p1 instanceof Promise) // true, object casted to a Promisep1.then(function(v) {console.log(v); // "fulfilled!"}, function(e) {// not called});
Promise.reject()
Promise.reject(reason) takes the “reason object” as a String or Error, and returns a promise that is in the rejected state with the appropriate rejection reason.
|
1 2 |
Promise.reject(new Error('fail')) .then(function() {ovaj deo je za successfully i ne poziva se}, function(error) { console.log(error); }); |
Promise.all()
Promise.all(iterable) takes an iterable list of promise objects as a parameter. The Promise.all() method returns one promise when all promises from the list have been successfully resolved. The Promise.all() method will return “promise”, even when passed an empty list or non-promise element (see example). With this method, the order of execution of the list of promises is not guaranteed, it is only guaranteed that it will return the last “promis”, when all the promises from the list are fullfiled.
Example
|
1 2 3 4 5 6 7 8 9 |
var p1 = Promise.resolve(3); var p2 = 1337; var p3 = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'foo'); }); Promise.all([p1, p2, p3]).then(values => { console.log(values); // Returns: [3, 1337, "foo"] }); |
In the event that one promise from the list is in the rejected state, the Promise.all() method returns a rejected promise, regardless of whether there is a successful promise in the list. The reason for the rejection is returned along with the rejected promise. In case there are several rejected offers, the reason given is from the first rejected offer!
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var p1 = new Promise((resolve, reject) => { setTimeout(resolve, 1000, 'one'); }); var p2 = new Promise((resolve, reject) => { setTimeout(resolve, 2000, 'two'); }); var p3 = new Promise((resolve, reject) => { setTimeout(resolve, 3000, 'three'); }); var p4 = new Promise((resolve, reject) => { reject('Ovaj promise je namerno odbijen'); }); var p5 = new Promise((resolve, reject) => { setTimeout(resolve, 4000, 'five'); }); Promise.all([p1, p2, p3, p4, p5]).then(values => { console.log(values); }).catch(reason => { console.log("Nama svih sastojaka", reason); }); // Returns: "Ovaj promise je namerno odbijen" |
Asynchrony and synchronicity of Promise.all()
Promise.all() behaves asynchronously in all cases except when an empty object is passed instead of a list, then it behaves synchronously.
Example
|
1 2 3 4 5 6 7 8 9 10 11 12 |
var prazanPromise = Promise.all([]); var listaPromise = [Promise.resolve("some information"), Promise.resolve(123)]; var nekiPromise = Promise.all(listaPromise); // Synchronous printing of output console.log(prazanPromise) // Returns: Promise { <state>: "fulfilled", <value>: Array[0] } console.log(nekiPromise); // Returns: Promise { <state>: "pending", <value>: undefined } // Asynchronous printing of output setTimeout(function(){ console.log(nekiPromise); // Returns: Promise { <state>: "fulfilled", <value>: Array[2] } }); |
Note that with direct synchronous printing, “emptyPromise” is in the fullfiled state, while the standard promise named “somePromise” is in the pending state.
When to use “Promise.all()” and when to use “Promise.prototype.then()”?
The choice boils down to whether the order of execution of the promises matters or not. In the following example, the order of promise execution is important, so Promise.prototype.then() is used:
|
1 2 3 |
nabaviDrvo() .then(() => napraviCamac()) .then(ploviRekom()); |
While in the following example for making concrete, the order in which promises will be completed is not important, but only the moment when they are ready is important, so it is recommended to use Promise.all():
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var nabaviSljunak = new Promise((resolve, reject) => { setTimeout(resolve, 1000, 'one'); }); var nabaviCement = new Promise((resolve, reject) => { setTimeout(resolve, 2000, 'two'); }); var nabaviAditiv = new Promise((resolve, reject) => { setTimeout(resolve, 3000, 'three'); }); Promise.all([nabaviSljunak, nabaviCement, nabaviAditiv]) .then(values => { //napravi beton }) .catch(reason => { console.log(reason) }); |
Promise.race()
Promise.race(iterable) is a method that also takes an iterable list of promise objects as a parameter and returns a promise. This method returns the result that “arrives first”, whether it succeeds or not. So a promise can be returned either with the final data of a successfully executed asynchronous operation or with the reason for a failed operation.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
var p1 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, "one"); }); var p2 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, "two"); }); Promise.race([p1, p2]).then(function(value) { console.log(value); // "two" // Both resolve, but p2 is faster }); //----------------------------------------------------- var p3 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, "three"); }); var p4 = new Promise(function(resolve, reject) { setTimeout(reject, 500, "four"); }); Promise.race([p3, p4]).then(function(value) { console.log(value); // "three" // p3 is faster, so it resolves }, function(reason) { // Not called }); //----------------------------------------------------- var p5 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, "five"); }); var p6 = new Promise(function(resolve, reject) { setTimeout(reject, 100, "six"); }); Promise.race([p5, p6]).then(function(value) { // Not called }, function(reason) { console.log(reason); // "six" // p6 is faster, so it rejects }); |

