Introduction to JavaScript thenable
Promises are a big part of the JavaScript environment which allows us to represent the possible completion or failure of an asynchronous operation and the result for each possibility
JavaScript thenable
is an object that holds or implements the then()
method and can have two callbacks, one for when the Promise is fulfilled, and one for when the Promise is rejected. All Promises are thenable
object, but not all thenable
objects are promises.
All thenable
object has a fundamental structure like the one below, and can have more properties than the then
method but must have the then
method to be called a thenable
object.
const obj = {
then() {
}
}
The then
method in the thenable
object accesses the result of a settled or failed promise via the callbacks it accepts. JavaScript allows us to use thenables
in place of promises via the use of the Promise resolve
method, as it tracks thenable
objects which all Promises have.
Promises in JavaScript
As stated earlier, Promises allow us to manage the completion or non-completion of an asynchronous operation.
With an example, let’s illustrate Promises and show you how it works. Say, we need to work with an API or URL, we can make use of a Promise
to deal with a successful or failed call using the resolve
and reject
callbacks, then
method to execute the resolve
callback, and the catch
to deal with the reject
callback.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Promise</title>
<script src="index.js" defer></script>
</head>
<body>
</body>
</html>
The JavaScript file, index.js
, will contain the code below
function getData(url) {
return new Promise((resolve, reject) => {
if (!url) {
reject("No URL provided");
}
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
xhr.onload = function () {
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(xhr.status);
}
};
});
}
const url = prompt("Enter a URL");
getData(url)
.then((result) => {
console.log("Success!");
console.log(result);
})
.catch((status) => {
console.log(`An error with status code ${status} occurred`);
});
Output
And after the OK
, the below
With this type of setup, we can have some guarantees and allows ourselves to chain multiple callbacks that will be invoked in the order they are inserted.
Using JavaScript thenables
Since you know how the then
methods work, we can store the then
method within an object making it a thenable
rather than work it directly with a function as in the code in the above section. Remember that we said all Promise objects are thenable
objects, we can work with the thenable
as a Promise object and work our code more directly.
So, by rewriting the same code, we can make use of the Promise resolve
method to call the then
method inside the obj
object (a thenable
object).
const obj = {
then(resolve, reject) {
const url = "<https://reqres.in/api/users?page=2>";
if (!url) {
reject("No URL provided");
}
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
xhr.onload = function () {
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(xhr.status);
}
};
},
};
Promise.resolve(obj).then((result) => {
console.log(result);
});
Output
{"page":2,"per_page":6,"total":12,"total_pages":2,"data":[{"id":7,"email":"michael.lawson@reqres.in","first_name":"Michael","last_name":"Lawson","avatar":"<https://reqres.in/img/faces/7-image.jpg>"},...
Chaining Promises
The fact that you can return a promise (or any thenable
) from a then/ catch/ finally handler to resolve the promise they create means that if you need to do a series of asynchronous operations that provide promises/thenables
, you can use then on the first operation and have the handler return the thenable
for the second operation, repeated as many times as you need. Here's a chain with three operations returning promises:
firstOperation()
.then(firstResult => secondOperation(firstResult)) // or: .then(secondOperation)
.then(secondResult => thirdOperation(secondResult * 2))
.then(thirdResult => { /* Use `thirdResult` */ })
.catch(error => { console.error(error); });
When that code runs, firstOperation
starts the first operation and returns a promise. Calls to then and catch set up handlers for what happens next. Later, when the first operation completes, what happens next depends on what happened to the first operation: if it fulfills its promise, the first fulfillment handler gets run and starts the second operation, returning the promise secondOperation
provides.
If the second operation fulfills its promise, that fulfills the promise the first then
returned, and the next fulfillment handler gets run: it starts the third operation and returns the promise of its result. If the third operation fulfills its promise, that fulfills the promise from the second then
. That calls the code that uses the third result. Provided that code doesn't throw or return a rejected promise, the chain completes. The rejection handler doesn't get run at all in that case, since there were no rejections.
If the first operation rejects its promise, though, that rejects the promise from the first then
, which rejects the promise from the second then
, which rejects the promise from the third then
, which calls the rejection handler at the end. None of the then
callbacks get called, because they were for fulfillment, not rejection.
If the first operation succeeds and its fulfillment handler starts the second operation and returns its promise, but the second operation fails and rejects its promise, that rejects the promise from the first then
, which rejects the promise from the second then
, which rejects the promise from the third then, which calls the rejection handler. Similarly, if the first operation succeeds but the fulfillment handler throws
, the same thing happens: the remaining fulfillment handlers are skipped, and the rejection handler runs.
Naturally, the same is true if the first and second operations fulfill their promises but the third rejects its promise (or the handler starting it throws).
If all three operations succeed, the final fulfillment handler is run. If that handler throws
, either directly or by calling a function that throws, the rejection handler gets run:
As you can see, a promise chain of this kind is very much like a try/ catch
block around three synchronous operations (and some final code), like this:
// The same logic with synchronous operations
try {
const firstResult = firstOperation();
const secondResult = secondOperation(firstResult);
const thirdResult = thirdOperation(secondResult * 2);
// Use `thirdResult` here
} catch (error) {
console.error(error);
}
Just as the separation of the main logic from the error logic is useful in try/ catch
synchronous code, the separation of the main (fulfillment) logic from the error (rejection) logic in promise chains is useful.
Summary
A “promise” is an object or function with a then method whose behavior conforms to [the Promises/A+ specification]. While a “thenable” is an object or function that defines a then method.
So all promises are thenables, but not all thenables are promises. An object might define a method called then that doesn't work as defined for promises; that object would be a thenable, but not a promise.
References
Using Promises - JavaScript | MDN (mozilla.org)
Promise - JavaScript | MDN (mozilla.org)
Promise.resolve() - JavaScript | MDN (mozilla.org)