I can’t Promise, but Probably should work.
•Hey folks!
Have you ever used something without understanding how it works?
- Then raise your right leg up (go ahead, do it really).
- Then raise your left leg up.
- Catch yourself if you fall down.
- Finally, continue reading the rest of this article.
Sounds familiar? Yup, JavaScript Promises.
I had some difficulty in understanding and using JavaScript Promise when it first appeared. Later I got comfortable using it, although the understanding was still missing. So, in an attempt to bring understanding, I tried to recreate the core functionality of Promise from scratch. It came out well and I realized that Promise was not all that hard!
I still like the good old callback functions by the way… Well, kind of… I don’t know... Whatever...
Just a side note: This is not an article about how exactly Promise works. Rather, this is an attempt to write asynchronous code in a synchronous way - just like Promise. Yet, doing so might make Promise clearer to you.
Let’s first look at how we use a Promise first.
// Create a Promise
const task = new Promise((resolve, reject) => {
// Use setTimeout to create asynchronicity
setTimeout(() => {
// Choose 0 or 1 randomly
const flag = Math.round(Math.random());
// Invoke resolve or reject handlers based on flag
flag ? resolve('Success!') : reject(new Error('Oops!'));
}, 1000);
});
// Add custom resolve and reject handlers
task.then(console.log)
.catch(console.error)
.finally(() => console.log('Meh...'));
A new Promise is created and passed in the executor function. I used setTimout to create an asynchronous operation and then called the resolve/reject handlers based on the value of the flag variable.
Wait a second... Are const variables variables? They can’t vary!
Next, the then, catch and finally methods are used to pass in the resolve, reject and "finally" handlers (I call it endTask) to the Promise. These handlers will be executed based on the output of the asynchronous executor function. This is the core of a JavaScript Promise.
Now, how can we do this ourselves, Probably?
Let’s start with a question. In the previous example, the executor function will be executed immediately after the Promise is created (that’s the requirement). But how does that work since we haven’t provided those resolve and reject parameters yet?!
Simple. Just create default resolve and reject handlers and pass them on to the executor. Once the executor is run and produces an output (or error) the default handlers should store them on the object. Whenever the user sets their own resolve and reject handlers, simply call those with the stored output. Ta-da… Just as Promised! This resolved the mystery of Promise for me.
Alright then, let’s recreate this baby (no, that’s not what I meant). But what do we name him? ...her? Anyway, I’m very modest about my programming skills. Hence, it’s not wise for me to Promise or Ensure or Guarantee or Swear or anything. Instead, I name it Probably. Yes, I just used an adverb as noun, because now I can cleverly say,
"Oh yeah... This Probably should work!"
Let’s jump into the code, the Probably constructor first. I still use the old-style function constructors to create objects (perhaps I’m not class-y). We won’t be doing error handling and other checks here in order to simplify the code (who’d wanna do error handling anyway? Such a waste of time...).
// The Probably constructor
function Probably(executor) {
// "Pending" is always the initial state
this.state = 'pending';
// Properties to store users' handlers
// from then, catch and finally methods
this.resolve = null;
this.reject = null;
this.endTask = null;
executor(
// Default resolve handler
(val) => {
this.value = val;
this.state = 'fulfilled';
this.resolve && this.resolve(val);
this.endTask && this.endTask();
},
// Default reject handler
(err) => {
this.reason = err;
this.state = 'rejected';
this.reject && this.reject(err);
this.endTask && this.endTask();
}
);
}
Just like Promises, Probably can have three states. "pending" is always the initial state. Later, the state changes to either "fulfilled" or "rejected" based on which default handler is called by the executor function. We also add three properties resolve, reject and endTask to store the respective handlers when the user adds them through then, catch and finally.
The executor function is run next and the default resolve and reject handlers are passed as parameters. When the result is obtained, first it gets stored in the object and then the related user provided handler is called and the result is passed. Note that for storing the result, we’re creating a property as either "value" or "reason" for resolve or reject respectively.
And next, we build the prototype for Probably.
Probably.prototype = {
constructor : Probably,
then : function(resolve, reject) {
this.resolve = resolve;
this.value && resolve(this.value);
reject && this.catch(reject);
return this;
},
catch : function(reject) {
this.reject = reject;
this.reason && reject(this.reason);
return this;
},
finally : function(endTask) {
this.endTask = endTask;
this.state !== 'pending' && endTask();
}
};
I think the prototype is self explanatory. Receive the resolve, reject and endtask handlers through then, catch and finally methods respectively and store them on the object instance. If the result or error from the executor function is available, just pass it on to the relevant handlers. Both then and catch returns the object instance to enable method chaining (then().catch().finally()). Note that this simple implementation doesn’t work with multiple then chains as seen in Promises - I’m not going that deep here. But yet, I made the then method accept the reject handler as well in order to make Probably behave like a Promise more.
And in the finally method, we check whether Probably has settled by looking at the state property. The state won't be "pending" if Probably has settled - so execute the endTask handler.
Now that we have set up Probably, let's see it in action. I'm using the same code we wrote for Promise.
Would it work? Probably.
// Create a Probably object
const task = new Probably((resolve, reject) => {
// Use setTimeout to create asynchronicity
setTimeout(() => {
// Choose 0 or 1 randomly
const flag = Math.round(Math.random());
// Invoke resolve or reject handlers based on flag
flag ? resolve('Success!') : reject(new Error('Oops!'));
}, 1000);
});
// Add custom resolve and reject handlers
task.then(console.log)
.catch(console.error)
.finally(() => console.log('Meh...'));
I'm not sure about you, but it works on my machine.
Nevertheless, I'm damn sure that this great article made you understand what it means to write asynchronous code synchronously, and shed light on what JavaScript Promises really are. Come back for more such great learning resources as I publish them (sorry, my modesty just took a tea break).
Cheers!
Related links
Creating a JavaScript promise from scratch: is a great series of articles from Nicholas C. Zakas if you want to learn all the nuts and bolts of the JavaScript Promise. A highly recommended read.