Promises have been there for a while now, officially released in ES6 but already being supported by most browsers before that and/or polyfiled by libraries. They are a game changer in Javascript, helping to make our code more functional and in result easier to maintain and read.
As Martin Fowler defined them
Promises are objects which represent the pending result of an asynchronous operation.
Yes, just using one line to explain all about Promises might not be enough! In this article I will try to clearly articulate what a Promise is, what problems is coming to solve and how we can use them.
If you are new to Javascript, check the callbacks post first. This will help you understand some of the problems with async functions.
A Promise is an object that will eventually contain the result of an asynchronous operation.
Our Promise can have one of three states:
A promise is settled if it is not pending. Once a Promise has settled, it cannot transition to any other state.
A Promise has a constructor that receives a function with 2 parameters.
function fetchFile(path) {
return new Promise(function (resolve, reject) {
$.get(path, resolve).fail(reject);
});
}
Note: When an error happens, a frequent pattern is to send an Error to the catch:
reject(Error("Data could not be found"));
Every Promise exposes a then function that will receive a callback. The callback will be called when the Promise is fulfilled.
fetchFile("userData.json")
.then(function (userData) {
console.log(userData.name);
})
.catch(function (error) {
console.log(error);
});
Then is made in such a declarative way, that it allows multiple then methods to be chained:
fetchFile("userData.json")
.then(function (userData) {
return userData.userName; //userdata.userName = 'Juan Allo Ron'
})
.then(function (userName) {
return "User Name: " + userName;
})
.then(function (msg) {
console.log(msg); //prints 'User Name Juan Allo Ron'
});
Also, Promises can be chained:
fetchFile("userData.json")
.then(function (userData) {
return fetchFile(userData.id + ".png");
})
.then(function (photo) {
console.log(photo);
});
In this example, after the first Promise is resolved a new Promise is called to fetch the user image. Notice that the Promise is returned, this will allow the execution of the rest of the then callbacks when this new Promise gets resolved.
Catch can be used to manage any error inside the async call or inside any then.
fetchFile("userData.json")
.then(function (userData) {
console.log(userData.name);
})
.catch(function (error) {
console.log(error);
});
It is important to highlight that you can use one catch to manage any error. If something fails in the async call or in any of the then calls, then catch will be called. So, any of the following will execute the callback registered in the catch:
1 - Reject in the Promise:
function failPromise() {
return new Promise(function (resolve, reject) {
reject(new Error("This will always fail"));
});
}
failPromise()
.then(function () {
//never gets called
})
.catch(function (error) {
console.log(error);
});
2 - Exception in then
fetchFile("userData.json")
.then(function (userData) {
throw new Error("failing in then");
})
.then(function () {
//this never gets called
})
.catch(function (error) {
console.log(error); //failing in then
});
Also, if an exception is thrown in the execution, then the Promise gets rejected with that exception error.
function exceptionPromise() {
return new Promise(function (resolve, reject) {
throw new Error("This will always fail");
});
}
exceptionPromise()
.then(function () {
//never gets called
})
.catch(function (error) {
console.log(error);
});
What if you wanted to execute multiple Promises at the same time and wait for all the answers? Promise.all will give you that ability:
Promise.all([fetchFile("userData.json"), fetchFile("someOtherData")])
.then(function (listOfResults) {
console.log(listOfResults[0]); //userData
console.log(listOfResults[1]); //someOtherData
})
.catch(function (error) {
console.log(error); //Promise.all is rejected if any of the elements are rejected
});
Race is similar as all, as it will let you depend on multiple promises, but it will get resolved as soon as any of those Promises is resolved.
Good use cases for race are timeouts or fallbacks:
Promise.race([
fetchFile("http://example.com/file.txt"),
delay(5000).then(function () {
throw new Error("Timed out"); //if 5000 ms passed before fetchFile resolves, then this will throw an error and make everything fail.
}),
])
.then(function (text) {
/*···*/
})
.catch(function (reason) {
/* ··· */
});
Promise.race([
fetchFile("http://myFastServer.com/userPhoto.png"),
fetchFile("http://backupServer.com/userPhoto.png"),
])
.then(function (userPhoto) {
console.log(userPhoto); //as only one Promise is resolved, then the data is from that Promise resolution
})
.catch(function (error) {
console.log(error);
});
The Promise Interface exposes two more methods that will let you return a resolved or failed Promise:
Using this utilities when can create a fetchFile with a cache:
var cache = new Map();
function fetchFile(path) {
const file = cache.get(path);
if(file){
Promise.resolve(file);
}
else {
return new Promise(function (resolve, reject) {
$.get(path, function (data) {
cache.set(path, data);
resolve(data);
}).catch(reject);
});
}
}
Notice that, when the file is in the cache there is no need for an async operation, but our fetchFile should still return a Promise, so the interface is not broken. This is where Promise resolve comes to the rescue.
A Promise is an ES6 class, so it can be easily extended:
class MyPromise extends Promise {
// use default constructor
success(resolve, reject) {
return this.then(resolve, reject);
}
failure(reject) {
return this.catch(reject);
}
}
Catch up with me on X (twitter):@juan_allo
---
@2024 Juan Manuel Allo Ron. All Rights reserved