Promise Fundamentals (part 1)
There once was a time when javascript did not natively support promises. As developers, we often take for granted that there was a time in the javascript world when this was true. Javascript did not have built-in promises, but it wasn't necessarily a common practice. When handling asynchronous code, a developer would often pass a callback. For example, it was common to find code like the following:
function myAsyncFunction(params, cb) {
setTimeout(() => {
cb('All done');
}, 1_000);
}
myAsyncFunction({}, function(res) {
console.log(res);
});
Callbacks worked fine for the most rudimentary scenarios. However, things started to get unwieldy quickly. In the scenario when the application would need to run multiple asynchronous functions, a pyramid of callbacks would need to be managed. Consequentially, the code would start to look like this hellish example:
function myAsyncFunction(params, cb) {
setTimeout(() => {
cb('All done');
}, 1_000);
}
myAsyncFunction({}, function (res) {
myAsyncFunction({}, function (res) {
myAsyncFunction({}, function (res) {
myAsyncFunction({}, function (res) {
myAsyncFunction({}, function (res) {
myAsyncFunction({}, function (res) {
...
});
});
});
});
});
Instead of stacking these callbacks into a pyramid. Could we write a class to manage these callbacks? The advantage of a class is that it would allow us to chain these callbacks.
Thanks to Promise/A+ specification, there is a way to do this, and it is possible with pure javascript. The specification outlines how to implement the concept of Promises. For asynchronized code, we can think of the callbacks as promises that get fulfilled when the code fulfills its task. The other fascinating ingredient of the specification is that it demands that the implementation of promises provide "an interoperable then method."
Concerning an example of the "then" method, we should revisit built-in promise syntax:
const pr = new Promise((resolve) => {
setTimeout(() => {
resolve('AllDone');
}, 1000);
})
pr.then(result => {
console.log(result);
// prints "All Done"
return "really done"
}).then(result => {
console.log(result);
// prints "really done"
});
With the advent of a then-able promise, we now solve two problems. We no longer need to pass a callback function to each asynchronous function; we can also chain each function. Let's see and try to write our version in pure javascript!
The first thing we will need is a class; let's call ours PromiseKeeper. Like the native promise, it'll receive a function, which will call the executor, and it will then pass a function into the executor that allows it access to set the resolved state. We will also set an internal state to "pending" (more on this later). The resolve function will receive a value. The value is to be saved as an instance state in our class. We will also add a "then" method, which receives a function similar to our constructor. However, we will wait to. So far, it will look like the following:
class PromiseKeeper {
constructor(executor) {
this.state = 'pending',
this.cb = null;
const resolve = value => {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value
this.cb(value);
};
executor(resolve);
}
then(fn) {
this.cb = fn;
}
}
const promise = new PromiseKeeper(resolve => {
setTimeout(() => {
resolve(true);
}, 2000);
});
Here is how we can run this code:
const promise = new PromiseKeeper(function(resolve) {
setTimeout(() => {
resolve(true);
}, 2000);
});
promise.then(res => {
console.log(`Then has been executed with ${res}`);
})
The looks very close to the Javascript built-in equivalent! Now we have something that is beginning to work, like built-in promises. However, it's not working as expected. According to the A+ specification, a "then" needs to be promise-compliant. In our current setup, we can't chain these functions. To do that, we need to make the then function create a promise. Instead of just calling the passed-in callback with the value, let's wrap it up as a PromiseKeeper object. We will also adjust our resolve function to call an unfulfilled method to signify to the child that the promise object has been fulfilled. The code now looks like the following:
class PromiseKeeper {
constructor(executor) {
this.state = 'pending',
this.cb = null;
const resolve = value => {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value
this.cb?.onFullfilled(this.value);
};
executor(resolve);
}
then(cb) {
return new PromiseKeeper(resolve => {
const handleCallback = () => {
const result = cb(this.value);
resolve(result);
}
if (this.state == 'pending') {
this.cb = {
onFullfilled: () => handleCallback()
}
} else {
handleCallback();
}
});
}
};
We can now chain our then promises very similarly to native promises.
const promise = new PromiseKeeper(resolve => {
setTimeout(() => {
resolve(true);
}, 2000);
});
promise.then(res => {
console.log(`First then fired ${res}`);
return 2;
}).then(res => {
console.log(`Second then fired ${res}`);
});
We have now created something that functions well, like A+ promises. However, much more must be done, which I hope to cover in more detail in the next blog post. Our promise currently lacks any exception handling and does not support the finally method. We will explore that implementation next.