Click here to Skip to main content
15,867,986 members
Articles / Web Development / Node.js

Building a Simple Promise Constructor

Rate me:
Please Sign up or sign in to vote.
4.73/5 (4 votes)
29 Jan 2021GPL327 min read 15.4K   1   2
The causality of a JavaScript promise constructor. Asynchronous callbacks vs Promise.
By reading this article, you should gain more insight into the internal promise and asynchronous callback mechanics, and be able to decide which one suits your problem better.

Introduction

"A low sugar alternative is to use plain callbacks in a pattern called Continuation Passing Style. This promiseless style may help you avoid what I like to call code diabetes, where important state is hidden behind syntax sugar and dangerous corner cases lurk." - Benoit Essiambre

Now it's time to look at JavaScript Promises. What they are and what they stand for. What they promise and what they deliver.

I've read many articles about Promises and stumbled on its various definitions, most of them mentioning the word proxy. I do know what a proxy server is, but what a proxy object or a proxy value is? That, I can only imagine.

Let's take a short journey and build ourselves a promise constructor. Not an academically approved and industry standard A+/Promise, nevertheless something that helps a lot to understand them. First, this journey starts with a callback...

Asynchronous

JavaScript is single threaded, non blocking and asynchronous. It has a call stack, a callback queue and an event loop. Now let's be honest here, each of those bolded out concepts requires an article of its own.

Texts like Understanding Asynchronous JavaScript or if you prefer videos, What the heck is the event loop anyway? are required.

You can think of an application executing on the computer as a process running in the operating system. Most applications run as a single process on the OS and most of them have only one executing thread. In case of JavaScript, that single thread of execution is going to be your script. Historically, JS programs execute in the browser application.

I assume for simplicity that the browser application is running as one process on the OS. How the browser application manages its threads is browser specific and there are many browsers but Firefox and Chromium.

Next simplification will be to assume that there is one thread of execution per HTML document. All the scripts referenced or embedded in that document are combined in one body and this united script is to be run in a single thread of execution. This is a model, not a fact.

People often think of asynchronous functions in a way that suggests those functions are time demanding. There are times when your own function can process data for quite a long time and that, by itself, doesn't make it asynchronous. We could write a function that multiplies extra large 3D matrices that run for a minute and it will still be executed synchronously.

And, by God, what does it takes a lot of time mean? Is 2 milliseconds a lot of time for a function? Is it 103 milliseconds? I've seen requests to a database that take up to 60 seconds...

It is hard to argue what function should be asynchronous and what should be not based only on time, because time is a quantity. Even simplest questions about time cannot be answered with YES or NO. It requires how much?

On the other hand, the questions: single threaded? and non-blocking? If evaluated to true raise the flag asynchronous. Whenever your executing script calls a functions that talks to other threads of the same process or other processes on yours or somebody else's computer, JavaScript preferably fires that function asynchronously.

For the purpose of this article, I will call asynchronous functions which deliver your data via an asynchronous callback function. You pass that callback as an argument to the called async function. Note the difference between an async function and an async callback.

JavaScript keeps track of functions, their order of execution and their data on the call stack. You put functions on the call stack by invoking them from your script. The called function is always put on the stack on top of the caller function.

There are two ways to get on the call stack. Synchronous functions directly start on the call stack, by some function that is already on it.

Asynchronous callbacks get on the call stack via the callback queue, if and only if the call stack is empty.

Every async function has some part of it synchronous that gets on the call stack, makes a request to some other thread and leaves the call stack in a relatively short amount of time. When the data is finally ready, as argument for your asynchronous callback, that callback will enter the callback queue at its last position.

The job of the event loop is to move the first callback from the event queue onto the call stack when it detects it empty. Mind the time gap. Async callbacks do not enter the call stack in the same time frame as the async function that requested the data. The call stack is emptied in between.

Another way of saying asynchronous callbacks in JavaScript is: functions that enter the call stack from the side of the callback queue.

Example 1

JavaScript
var fs = require("fs");

fs.readFile("file.txt", {encoding: "ascii"}, function(err, data1) {
    if (err) { console.trace(err); return }

    var file2 = data1.split(' ')[1] + ".txt";
    fs.readFile(file2, {encoding: "ascii"}, function(err, data2) {
        if (err) { console.trace(err); return }

        var file3 = data2.split(' ')[2] + ".txt";
        fs.readFile(file3, {encoding: "ascii"}, function(err, data3) {
            if (err) { console.trace(err); return }

            console.log(data2);
            console.log(data3);
        });
    });
});

The files needed for this example to work are in the files.tar.gz archive. So is the final version of the promise constructor.

This simple example executed in Node.js demonstrates every day asynchronous code. We issue an async request. When the data is ready, our callback is called passing to it that data. Then we extract the second word from the data and we use it to produce our file name. We issue another file read and so on... Until, for the third time, we finally read the last file and print it, but I also print the second file. ;)

The code is clear and a faithful representation of what goes in the computer. Opponents of asynchronous callbacks call this callback hell. This specific source code indenting that appears when dealing with callback hell they fancy calling the pyramid of doom.

First of all, we can get rid of the indenting if we un-nest the callback functions.

Example 2

JavaScript
var fs = require("fs");

function read1(err, data1) {
    if (err) { console.trace(err); return }

    var file2 = data1.split(' ')[1] + ".txt";
    fs.readFile(file2, {encoding: "ascii"}, read2);
}

function read2(err, data2) {
    if (err) { console.trace(err); return }

    var file3 = data2.split(' ')[2] + ".txt";
    fs.readFile(file3, {encoding: "ascii"}, read3);
}

function read3(err, data3) { console.log(data3); }

fs.readFile("file.txt", {encoding: "ascii"}, read1);

Looks nicer? Now it has the same indenting of the equivalent code that uses promises. Although, example 1 and example 2 are not the same thing. Proper source code indenting represents its 2D structure. It's a reminder that when the last callback is called in example 1, I can print file3, but I can also print file2. You cannot print file2 in the second example in the function where you print file3.

Chronology is very important for understanding asynchronous callbacks. I would like to go through those two examples with the proposition that every line of synchronous code is taking 1 millisecond to execute and that all asynchronous callbacks take 100 milliseconds to reach the callback queue. Let's assume that the event loop moves the first callback in the event queue into the execution stack in zilch milliseconds.

The examples start with the fs.readFile function which wants to read data from a file called "file.txt".

In example 1, this function takes 1 millisecond to finish and return. Nothing happens for 100 milliseconds, from our point of view, although the Node.js runtime has employed a worker thread to finish our job with the OS and read that file.

After those 100 milliseconds are wasted suddenly our callback defined as the last argument to the fs.readFile function is placed on the callback queue in the 101st millisecond. There was nothing on it, so our callback is first to enter and first to leave. The event loop "sees" that and moves our async callback from the queue onto the stack.

Notice the 101 milliseconds period that passed since we called fs.readFile to the point in time when our async callback entered the callback queue. Also notice the 100 milliseconds that the call stack was empty since the fs.readFile function returned. This time gap and the fact that async callbacks cannot enter the execution stack while it is used by the very functions that were on it and called for asynchronous action is the reason you don't usually see callbacks explicitly return values.

On with the execution of our example. The async callback entered execution in the 101st millisecond and is running its first line of code which finishes on the 102nd millisecond. That would be the if statement, checking for error.

Next, it will do some data processing and extract the name of the next file to be read into variable file2 by the end of the 103rd millisecond. A new call to an fs.readFile on the 104th millisecond and the work goes to another thread...

Another 100 milliseconds pass from the time that fs.readFile has returned in the 104th millisecond and suddenly in the 204th millisecond, our callback enters the callback queue and immediately goes on the call stack...

Much of all this repeats till file2 and file3 are printed in the 308th and 309th millisecond respectively.

Promises

There was a lot of fanfare when Promises were finally introduced as standard in JavaScript. It was at the time of official ES6 standard, also announced with fanfare. People started getting proud and bold with the ES6 additions to the language. Almost to the extent of their Java peers. I don't know if this is because of the class keyword or other factors...

It is to be noted here that all examples in this text will adhere to the ES5 standard of JavaScript, except for the usage of the Promise object that comes with ES6.

Let's rewrite an equivalent of example 1 with promises.

Example 3

JavaScript
var fs = require("fs");

new Promise(function(resolve, reject) {
    fs.readFile("./file.txt", {encoding: "ascii"}, function(err, data1) {
        if (!err) {
            resolve(data1);
        } else {
            reject(err);
        }
    });
}).then(function(data1) {
    var file2 = "./" + data1.split(' ')[1] + ".txt";
    return new Promise(function(resolve, reject) {
        fs.readFile(file2, {encoding: "ascii"}, function(err, data2) {
            if (!err) {
                resolve(data2);
            } else {
                reject(err);
            }
        });    
    });
}, function(err) {
    console.trace(err);
}).then(function(data2) {
    var file3 = "./" + data2.split(' ')[2] + ".txt";
    return new Promise(function(resolve, reject) {
        fs.readFile(file3, {encoding: "ascii"}, function(err, data3) {
            if (!err) {
                resolve(data3);
            } else {
                reject(err);
            }
        });    
    });
}, function(err) {
    console.trace(err);
}).then(function(data3) {
    console.log(data3);
}, function(err) {
    console.trace(err);
});

I don't know if I'm doing something wrong, but this looks uglier to me. Of course, beauty is subjective. "Gone" is the indentation of example 1, but so is the ability to print file3 and file2 at the same place. Unless, you resolve the third promise with an array like this: resolve([data2, data3]).

In example 1, I could have easily printed data1 and data3 at the end. To do that within example 3, I would have to always push the data in an array that I would have to pass along resolving in each new promise. Extracting information from that array would also require a line or two...

The Promise constructor returns an object that has a then method. This method has two parameters. The first is a function that deals with the successful read of data. The second is also a function, but this one deals with the possible error of the asynchronous function passed in to the Promise constructor. The then method also returns a new promise. In the above example, we use that returned promise from then to chain then actions.

You see when talking about promises it's handy to consult Dr. Dan Streetmentioner's book for dealing with the grammar of all future possible proxy resolutions. In reality, the first parameter to then is a function that wioll haven be-executed in case the call to fs.readFile would have been successful and the second parameter is a function that mayan errored on-when the call to fs.readFile would have been erroneous. This, even if the rest were true, which it isn't, is plainly impossible, say the doubters.

Jokes aside, I want the examples to be as simple as they can so we can concentrate on what is going on with promises. Therefore, I am going to exclude the scenarios that deal with the possible errors (willan on-errored) of the asynchronous API function. Here are all three examples without the error dealing code.

Example 4

JavaScript
var fs = require("fs");

fs.readFile("file.txt", {encoding: "ascii"}, function(err, data1) {

    var file2 = data1.split(' ')[1] + ".txt";
    fs.readFile(file2, {encoding: "ascii"}, function(err, data2) {

        var file3 = data2.split(' ')[2] + ".txt";
        fs.readFile(file3, {encoding: "ascii"}, function(err, data3) {

            console.log(data2);
            console.log(data3);
        });
    });
});

Example 5

JavaScript
var fs = require("fs");

function next1(err, data1) {
    var file2 = data1.split(' ')[1] + ".txt";
    fs.readFile(file2, {encoding: "ascii"}, next2);
}

function next2(err, data2) {
    var file3 = data2.split(' ')[2] + ".txt";
    fs.readFile(file3, {encoding: "ascii"}, next3);
}

function next3(err, data3) { console.log(data3); }

fs.readFile("file.txt", {encoding: "ascii"}, next1);

Example 6

JavaScript
var fs = require("fs");

new Promise(function(resolve) {
    fs.readFile("./file.txt", {encoding: "ascii"}, function(err, data1) {
        resolve(data1);
    });
}).then(function(data1) {
    var file2 = "./" + data1.split(' ')[1] + ".txt";
    return new Promise(function(resolve) {
        fs.readFile(file2, {encoding: "ascii"}, function(err, data2) {
            resolve(data2);
        });    
    });
}).then(function(data2) {
    var file3 = "./" + data2.split(' ')[2] + ".txt";
    return new Promise(function(resolve) {
        fs.readFile(file3, {encoding: "ascii"}, function(err, data3) {
            resolve(data3);
        });    
    });
}).then(function(data3) {
    console.log(data3);
});

Our code is now 100% optimistic. We don't need the reject parameter.

Let's make an example that uses already defined functions we pass as arguments to the respective then methods. This time, clearly assign each created promise from the then methods into its own variable.

Example 7

JavaScript
var fs = require("fs");

function executor1(resolve) {
    function callback(err, datac) {
        resolve(datac);
    }
    fs.readFile("file.txt", {encoding: "ascii"}, callback);
}

function next1(data) {
    function executor2(resolve) {
        function callback(err, datac) {
            resolve(datac);
        }
        var file2 = data.split(' ')[1] + ".txt";
        fs.readFile(file2, {encoding: "ascii"}, callback);
    }
    return new Promise(executor2);
}

function next2(data) {
    function executor3(resolve) {
        function callback(err, datac) {
            resolve(datac);
        }
        var file3 = data.split(' ')[2] + ".txt";
        fs.readFile(file3, {encoding: "ascii"}, callback);
    }
    return new Promise(executor3);
}

function next3(data) { console.log(data) };

var promise1 = new Promise(executor1);
var promise2 = promise1.then(next1);
var promise3 = promise2.then(next2);
var promise4 = promise3.then(next3);

Compare example 7 and example 5, where I have changed the function names to correspond accordingly. Ahh, the atrocities of object oriented programming... Why people feel better if they stuff the next functions into a then method, compared to old school procedural execution?

I mentioned earlier when explaining example 1, mind the time gap with example 7. Using the analogy of executing synchronous code in 1 millisecond pre line, no matter how complicated that line is, you get all the promises: promise1, promise2, promise3 and promise4 created in 4 milliseconds. Then, at the 5th millisecond, the call stack is emptied. There is nothing more to execute on it for a fairly large amount of time.

Function executor1 being passed to the Promise constructor is executed inside of it and that triggers a Node.js working thread to talk to the OS and get the file contents of "file.txt". The JavaScript runtime will return with your data in the callback function defined inside executor1. From the time promise1 is created on line 1, to the time the callback function has entered the callback queue, it will pass our agreed 100 milliseconds. On the second line of execution, promise2 is returned from promise1.then method where we declare to promise1 that we want to call function next1 when promise1 resolves.

Now that callback inside executor1 starts running in the 101st millisecond and in turn calls resolve with its datac in the 102nd millisecond, which we said that we want it to execute function next and the only executable line in next1 is return new Promise(executor2), thus by our model, that line is going to be executed in the 103rd millisecond.

It is becoming quite clear that the promise returned from next1 is no way going to be promise2. In case you were thinking that, because promise2 happens in the 2nd millisecond of execution and the promise returned from next1 happens in the 103rd millisecond of execution in example 7.

Further explanation is impossible without us creating our promise constructor. However, for simplicity, let's consider a scenario where only one asynchronous function is called.

Example 8

JavaScript
var fs = require("fs");

fs.readFile("file.txt", {encoding: "ascii"}, function(err, data1) {

    var file2 = data1.split(' ')[1] + ".txt";
    console.log(file2);
});

Constructor

"What I cannot create, I do not understand." - Richard Feynman

Example 9

JavaScript
var fs = require("fs");

function P(executor) {
    var _then;
    
    var resolve = function(data) {
        _then(data);
    }
    
    this.then = function(next) {
        _then = next;
        return this;
    }

    executor(resolve);
}

var P1 = new P(function (resolve) {
    fs.readFile("file.txt", {encoding: "ascii"}, function(err, data1) {
        resolve(data1);
    });
});

P1.then(function(data1) {
    var file2 = data1.split(' ')[1] + ".txt";
    console.log(file2);
});

//or should I say

function executor1(resolve) {
    function callback(err, data1) {
        resolve(data1);
    }
    fs.readFile("file.txt", {encoding: "ascii"}, callback);
}

function next1(data1) {
    var file2 = data1.split(' ')[1] + ".txt";
    console.log(file2);
}

P11 = new P(executor1);
P11.then(next1);

The simplest promise constructor, without any error checking and non-conforming to any standard. But, it works!

Let's modify it a little bit to make room for some constructive thoughts on object oriented programming. Those of you who dislike digression can safely skip the next head of the article and go straight to promise construction.

Programming for the Masses, not the Classes

"Every language that uses the word class, for type, is a descendent of Simula. Kristen Nygaard and Ole-Johan Dahl were matemathicians and they didn't think in terms of types, but they understood sets and classes of elements and so they called their types, classes. And basically in C++ as in Simula, a class is a user defined type." - Bjarne Stroustrup at Artificial Intelligence Podcast

Example 10

JavaScript
var fs = require("fs");

function Q(executor) {
    this._then = function() { console.log("dummy") };
    this.foo = function(data) {
        this._then(data);
    }

    this.then = function(next) {
        this._then = next;
        return this;
    }

    executor(this.foo);
}

var Q1 = new Q(function (resolve) {
    fs.readFile("file.txt", {encoding: "ascii"}, function(err, data1) {
        resolve(data1);
    });
});

Q1.then(function(data1) {
    var file2 = data1.split(' ')[1] + ".txt";
    console.log(file2);
});

Woe is us, this thing doesn't work! It says this._then is not a function. It surely is not, unless you have defined a function named _then in the global scope.

JavaScript's fairly functional nature makes this even more visible. You see that anonymous function which is being assigned to this.foo? When the runtime executes constructor Q it interprets this function or it just-in-time compiles it, whatever. Then it will assign the address of that function to the newly created object identified this inside the Q constructor as it's foo property. The so called foo method.

Couple of lines down, it will execute the executor function, the one we pass into the constructor, correctly passing the reference this.foo to the executor, but that reference is just an address of a function.

The way I have defined the executor function it uses an argument called resolve as a function. I could have named that argument any other way, for instance bar, but it would have always represented the same function as the object Q1's foo method. foo would have been always the correct function to call, but this time in a wrong context.

One of the most important notions in JavaScript, concerning its execution of functions, is the context. The context is the value of this.

You can think of this as being a special variable that you cannot directly assign to, but can do something about it with usage of Function.prototype's methods like: bind, apply and call.

Whenever JavaScript sees syntax like you were trying to (mayan have on-try) execute a method of an object, it immediately switches the value of this to be that object and then jumps to the execution of that function. Upon return from that object's method, the old value of this is popped.

So much for OOP in JavaScript. Doesn't that make you feel naked in front of your data? Good, because that's the way it should be. Any person telling you that you have to build a Byzantine hierarchy of classes between you and your data and that you should have to delegate bureaucracy is a ...

If you think, "Aha, I always said that JS is phoney, but my proper class based language is not!", think about this. In your's C++ and Java truly, when you are calling a method of an object, you are in fact passing that object as an argument to a function.

I'll use ANSI/ISO C++98 to illustrate my point.

Example 11

C++
#include <iostream>

class Cat {
public:
    Cat(int inital);
    int itsEnergy();
    void Eat(int e);
private:
    int energy;
};

int Cat::itsEnergy() {
    return this->energy;
}

void Cat::Eat(int e) {
    this->energy += e;
}

Cat::Cat(int inital) {
    this->energy = inital;
}

int main() {

    Cat Tom(5);
    std::cout << Tom.itsEnergy() << std::endl;
    Tom.Eat(11);
    std::cout << Tom.itsEnergy() << std::endl;
    
    return 0;
}

If you are thinking you are sending Eat messages to the Tom object..

You are calling a function whose true nature in C looks something like this: void cat_eat(Cat *this, int e) { this->energy += e; }

To have a glimpse of the truth, just replace this->energy with this.energy in Cat's first "method" itsEnergy. You will be greeted with an error:

[error: request for member ‘energy’ in ‘this’, which is of pointer type ‘Cat* const’ 
(maybe you meant to use ‘->’ ?)]

It's a constant pointer to a not so constant Cat. The size of Tom being equal to whatever that solely data member energy is. On my machine, it's a 4 byte object, an integer. I didn't put object in quotes, because it really is an object. An object comprised only of data. In programmer terms, the class is a data type.

What we are dealing with here is, in its most basic level, at least the ability to have namespaces. The namespace of the class Cat, that of the Dog, as well that of the Mammal and StrayCat. All objects being data.

If you are using virtual functions to do the famous runtime type polymorphism, you are embedding in the object one more hidden data member. A pointer to the table of virtual functions for the respected namespace.

If you create a polymorphic Cat object, you embed a pointer to the table of virtual functions from the Cat namespace, so when you have that Cat assigned to a Mammal type pointer, it can easily dereference the mapped call to the exactly named virtual function of the Cat class.

To make example 10 a working one, we just have to bind the function this.foo to the newly created object inside the Q constructor like this: executor(this.foo.bind(this))

Example 12

JavaScript
var fs = require("fs");

function Q(executor) {
    this._then = function() { console.log("dummy") };
    this.foo = function(data) {
        this._then(data);
    }

    this.then = function(next) {
        this._then = next;
        return this;
    }

    executor(this.foo.bind(this));
}

var Q1 = new Q(function (resolve) {
    fs.readFile("file.txt", {encoding: "ascii"}, function(err, data1) {
        resolve(data1);
    });
});

Q1.then(function(data1) {
    var file2 = data1.split(' ')[1] + ".txt";
    console.log(file2);
});

That binding will create another function from which inside, JavaScript will call the function we know referenced as Q1.foo and resolve, but before calling the function it will play the trick of switching the value of this to be equal to Q1.

That works something like this.

Example 13

JavaScript
var obj = { value: 4, see: function() { console.log(this.value) }}
var fSee = obj.see
obj.see()                    //4
fSee()                       //undefined
var bSee = fSee.bind(obj)
bSee()                       //4
bSee == obj.see              //false
bSee == fSee                 //false
obj.see == fSee              //true

More Promises

The purpose is to build a promise constructor that will replace the ES6 Promise constructor for the given rudimentary examples. So, let's move on and add all the async calls to the fs.readFile function.

Example 14

JavaScript
var fs = require("fs");

function P(executor) {
    var _then;
    
    var resolve = function(data) {
        _then(data);
    }
    
    this.then = function(next) {
        _then = next;
        return this;
    }

    executor(resolve);
}

var P1 = new P(function (resolve) {
    fs.readFile("file.txt", {encoding: "ascii"}, function(err, data1) {
        resolve(data1);
    });
});

P1.then(function(data1) {
    var file2 = "./" + data1.split(' ')[1] + ".txt";
    return new P(function(resolve) {
        fs.readFile(file2, {encoding: "ascii"}, function(err, data2) {
            resolve(data2);
        });    
    });
}).then(function(data2) {
    var file3 = "./" + data2.split(' ')[2] + ".txt";
    return new P(function(resolve) {
        fs.readFile(file3, {encoding: "ascii"}, function(err, data3) {
            resolve(data3);
        });    
    });
}).then(function(data3) {
    console.log(data3);
});

Instead of printing file3, it prints "file.txt". All the new "promises" created in then methods are lost into thin air.

Because we are returning the same object all the time, its _then value becomes overwritten 3 times and at the last call to P1's then method it finally becomes the function that logs the data. All this happens way before the async callback kicks in and calls the resolve function, passing the data read from the first file. Then the last function set by the then method is invoked just printing that data. This promise constructor only has short term memory.

Now we are going to take this further down the line in baby steps. The best data type for our problem here is the array.

We were unable to save the functions that represent our consequent steps in the processing of data because we had only one saving placeholder. Now, with an array, we are going to have as many placeholders as we wish.

Pushing down functions into the array of the "promise" in their needed order of execution via the then method, but removing them from the array when the resolve function is called by shifting. To have them in the same order they were stuffed in. More like a queue, not like a stack.

Example 15

JavaScript
var fs = require("fs");

function P(executor) {
    var _then = [];
    
    var resolve = function(data) {
        var f = _then.shift();
        f(data);
    }
    
    this.then = function(next) {
        return this;
    }

    executor(resolve);
}

var P1 = new P(function (resolve) {
    fs.readFile("file.txt", {encoding: "ascii"}, function(err, data1) {
        resolve(data1);
    });
});

P1.then(function(data1) {
    var file2 = "./" + data1.split(' ')[1] + ".txt";
    return new P(function(resolve) {
        fs.readFile(file2, {encoding: "ascii"}, function(err, data2) {
            resolve(data2);
        });    
    });
}).then(function(data2) {
    var file3 = "./" + data2.split(' ')[2] + ".txt";
    return new P(function(resolve) {
        fs.readFile(file3, {encoding: "ascii"}, function(err, data3) {
            resolve(data3);
        });    
    });
}).then(function(data3) {
    console.log(data3);
});

This executes a little bit and then it errors, saying f is not a function. In a timely manner, all functions passed to then method are stuffed into the array. Then, the call stack is emptied. Some time after this, fs.readFile finishes its job (our proposed 100 milliseconds) and the first callback is pushed on the callback queue.

The event loop moves the callback from the queue to the call stack. The callback starts execution with the data1 argument, the contents of "file.txt". The resolve function inside our anonymous callback is executed taking away from the _then array of the "promise" its first function (the 0th element).

That function is assigned to f and is executed passing data1. So far so good. The first stuffed function is using data1 to extract the next name of the file that has to be read. It is creating a new "promise" making inside of it the new async call to read file2 and returns that promise into thin air...

When that second anonymous "promise" finally finishes its async job (about another 100 milliseconds later) and is happily passing its data2 to its resolve function, that resolve function is trying to extract from its associated array the next procedure what to do with data2, something breaks.

We were stuffing procedures only to our firstly created "promise" called P1.

One way to solve this Gordian knot would be to tell all the newly created "promises" that are returned into thin air to resolve to the P1's own resolve function.

Either that, or let P1 tell the newly created "promises" what are the next steps stuffed into its _then array. This way, there has to be a call to the newly created and returned "promises" then method. That would change the structure of the code. The way we use the "promise" vs the way we use the built Promise from ES6.

We are taking the cowards way of telling the newly created promises to resolve to the one and only promise that knows what has to be done step by step to resolve this whole mess.

One little problem is that the P constructor function has its resolve function embedded into its own closure when it was invoked to create P1. To get it out, we need a function defined inside P and assigned to this that will return the address of resolve from whatever object we create, so we can pass it around.

Instead of writing a function like that, let's make resolve part of the P1 object, by assigning it to this inside of the constructor. We'll move the reference of resolve from the closure into the object. The object is also part of that closure, but that's a different story.

Example 16

JavaScript
var fs = require("fs");

function P(executor) {
    var _then = [];
    
    this.resolve = function(data) {
        var f = _then.shift();
        f(data);
    }
    
    this.then = function(next) {
        _then.push(next);
        return this;
    }

    executor(this.resolve.bind(this));
}

var P1 = new P(function (resolve) {
    fs.readFile("file.txt", {encoding: "ascii"}, function(err, data1) {
        resolve(data1);
    });
});

P1.then(function(data1) {
    var file2 = "./" + data1.split(' ')[1] + ".txt";
    return new P(function(resolve) {
        fs.readFile(file2, {encoding: "ascii"}, function(err, data2) {
            P1.resolve(data2);
        });    
    });
}).then(function(data2) {
    var file3 = "./" + data2.split(' ')[2] + ".txt";
    return new P(function(resolve) {
        fs.readFile(file3, {encoding: "ascii"}, function(err, data3) {
            P1.resolve(data3);
        });    
    });
}).then(function(data3) {
    console.log(data3);
});

This does the job, but you know that this is just a dirty hack. It has several problems.

Firstly, if I haven't returned the first "promise" to variable P1, I couldn't have its resolve method passed to those newly created "promises".

Sending those "promises" into thin air also feels bad. Though, they do their job. When the async function inside of them is invoked, it is calling the right resolve method. The only one that knows the stuffed then's inside the array.

To make matters worse, you are not supposed to return the same and only promise from a then method. That's not what the real Promise is doing from out ES6 example. Every then method in example 6 returns a new promise to which we chain another promises via its then method and so on. Bloody hell, there goes our array...

By the Book

Promises anywhere. everywhere. nowhere...

Example 17

JavaScript
var fs = require("fs");

function P(executor) {
    var _then;
    
    this.resolve = function(data) {
        this.resolvePromise = _then(data);
    }
    
    this.then = function(next) {
        _then = next;
        this.thenPromise = new P();
        return this.thenPromise;
    }

    if (typeof executor == "function") executor(this.resolve.bind(this));
}

var P1 = new P(function (resolve) {
    fs.readFile("file.txt", {encoding: "ascii"}, function(err, data1) {
        resolve(data1);
    });
});

P1.then(function(data1) {
    var file2 = "./" + data1.split(' ')[1] + ".txt";
    return new P(function(resolve) {
        fs.readFile(file2, {encoding: "ascii"}, function(err, data2) {
            resolve(data2);
        });    
    });
}).then(function(data2) {
    var file3 = "./" + data2.split(' ')[2] + ".txt";
    return new P(function(resolve) {
        fs.readFile(file3, {encoding: "ascii"}, function(err, data3) {
            resolve(data3);
        });    
    });
}).then(function(data3) {
    console.log(data3);
});

Now the situation is even more crazy. We have "promises" flying from two different places. One of them knowing what to do next, the other responsible for the data. We need to close the gap, zip it.

We have to get the argument for the then method and set it to the other "promise" that is returned in the future to resolve with the data.

The "promise" returned from then has nothing to do. We don't have an executor for it. It's only job is to steal your procedure that is going to process (wioll haven be-processen) your future data. An IF will do the job.

Let's see what's happening, using our 1ms and 100ms model. Firstly, P1 is created issuing async fs.readFile request that is going to be finished in 100ms. A millisecond after that, P1.then method is called with an argument that is a function. That function is going to massage the data when is ready and extract the file name making a second async fs.readFile call, returning a new "promise".

The then method is going to record the function that is supposed to process the data into P1's _then placeholder.

In the next millisecond P1.then is going to produce a new "promise", this one a complete dummy, and return it. We shall call this "promise", the thenPromise. Press pause, for now.

Only 4-5 milliseconds have passed. We have 2 promises. We have one async callback running in approx 95 milliseconds. That callback is going to call P1.resolve with the data. This is going to resolve to calling the _then function which extracts the data, make a new promises, and returns it.

Where is this returned promise going to pop?

Inside P1.resolve. We better save this returned promise, because this is where the data from the secondly read file is going to show up. We are going to call this the resolvePromise.

So, now suddenly, in P1 we have a hold on two promises. The one with the future data and the exact one that is supposed to tell that promise what to do with its data.

Example 17 reads the second file and then cracks with a TypeError "_then is not a function" at this place this.resolvePromise = _then(data). We are missing one pice of vital information. We have to copy the _then reference from the thenPromise into the _then reference of the resolvePromise.

And let's go a step ahead of ourselves to save on ink and paper. To zip it all, we have to copy the reference to the thenPromise.thenPromise into resolvePromise.thenPromise, too.

When the resolvePromise itself resolves, it will have to know its own thenPromise to copy its _then reference.

In the run of the script, relatively long time before the first async callback is placed on the callback queue, you can only present the separate procedures for what you would like to do to the data. There is no possible way to touch that data. Those procedures are stuffed inside a linked list of promises. All those promises have nothing to do except to capture the "declaration" (so to speak) of future data processing procedures.

No mater how many promises you chain, once you will reach the end. The call stack will be emptied and then the real action begins.

New promises start to appear from the other side, like in a mirror, for every async function that requested something. The callbacks of those async functions have the data (or the error), but they don't know what to do with it. Our job was to entangle those promises, the one with the data and its respected one with the code, just like in quantum entanglement.

I am going to change the _then placeholder. Now, it is a more than private variable existing in a closure. To manipulate its value outside of the constructor P more easily and since I'm not that crazy about encapsulation, I'm going to replace var _then with this.thenFunction. This way, we won't have to write accessor methods.

Press play on tape and let's translate the said into code.

The final version of promise for this article.

Example 18

JavaScript
var fs = require("fs");

function P(executor) {

    this.resolve = function(data) {
        if (typeof this.thenFunction == "function") {
            this.resolvePromise = this.thenFunction(data);
            if (this.resolvePromise && this.resolvePromise.constructor == P) {
                this.resolvePromise.thenFunction = this.thenPromise.thenFunction;
                this.resolvePromise.thenPromise = this.thenPromise.thenPromise;
            }
        }
    }
    
    this.then = function(next) {
        this.thenFunction = next;
        this.thenPromise = new P();
        return this.thenPromise;
    }
    
    if (typeof executor == "function") executor(this.resolve.bind(this));
}

new P(function(resolve) {
    fs.readFile("./file.txt", {encoding: "ascii"}, function(err, data1) {
        resolve(data1);
    });
}).then(function(data1) {
    var file2 = "./" + data1.split(' ')[1] + ".txt";
    return new P(function(resolve) {
        fs.readFile(file2, {encoding: "ascii"}, function(err, data2) {
            resolve(data2);
        });    
    });
}).then(function(data2) {
    var file3 = "./" + data2.split(' ')[2] + ".txt";
    return new P(function(resolve) {
        fs.readFile(file3, {encoding: "ascii"}, function(err, data3) {
            resolve(data3);
        });    
    });
}).then(function(data3) {
    console.log(data3);
});

One lesson learned here by me is that verbal language description is much more verbose than code.

Second Thoughts

What happens when you call resolve before then?

Example 19

JavaScript
var fs = require("fs");

var rFile = new Promise(function(resolve, reject) {
    fs.readFile('./file1.txt', {encoding: 'ascii'}, function(err, data) {
        resolve(data);
    });
});

setTimeout(rFile.then, 3000, function(message) {console.log("message")});

On node 6.2.2, after 3 seconds, we get the error:

timers.js:333
      ontimeout = () => callback.call(timer, arguments[2]);
                                 ^
TypeError: #<Timeout> is not a promise

On node 4.0.0, the error is:

timers.js:89
        first._onTimeout();
              ^
TypeError: [object Object] is not a promise

Node 0.12.0:

timers.js:223
      callback.apply(timer, args);
               ^
TypeError: [object Object] is not a promise

Interesting implementation detail...

I really don't see the benefits of using a Promise vs async callbacks (especially in the form of example 2).

Some higher level language experts say it is a synchronization point. It certainly is synchronizing​​​​ your previously written code with your future input data. That's not much different from what programming has always been.

Some say with async callbacks you don't know if your current code is going to finish first or your data that was requested asynchronous is going to pop first. Well, that may be the case in those other high level languages that maybe execute their programs in parallel threads.

In JavaScript's model of runtime, that is simply impossible. Your current code is going to finish first and then the async callbacks can enter the execution stack. That's perfectly synchronous. ;)

There are a lot of documents on the web glorifying how you get the control back with promises, how you know when things are done... In JavaScript, things are never going to change the way they are done. They are just going to be hidden from you.

I prefer languages, libraries and frameworks that have transparency. From sky high to dirt low.

If there is a benefit of using promises, there also must be a flip side. That's the way things are.

One thing to note about Promise in ES6 is that "Once a Promise is fulfilled or rejected, the respective handler function (onFulfilled or onRejected) will be called asynchronously (scheduled in the current thread loop)." From MDN Web Docs. In my manner of speaking so far, fulfilled means resolved and the only handler mentioned is onFulfilled, that is the argument function passed to then method of constructor P.

One calls a function asynchronous simply by using the setTime function provided by the runtime. The bulk of setTime is in another thread so whatever you pass to it, it will come via the callback queue, effectively making it be "called asynchronously (scheduled in the current thread loop)". If I'm reading this right, I cannot find the reason for it. Maybe it has something to do with the ES2017 additions of the language like async and await.

The keyword async makes a function execute asynchronously in such a way that it will get to the callback queue after all expressions/functions inside of it marked with await enter the callback queue. I'm making this assumption before preparing an article about async & await. It's a good thing that I can correct this article when I understand things better (mayan on-uderstand re-correcten).

There is more to a real Promise than what I have coded in every 16 examples up there. On the other hand, I hope after reading this, you will be ready not just to use promises, but to reason about them and their quirks.

I have left the error cases and handy Promise methods like all for homework. The flag for the promise state: pending, rejected, fulfilled... is also for homework. I find the state of promises to be just a byproduct of their functionality, nothing of importance. On the other hand, Promise methods like all are important and interesting to construct.

Happy coding!

History

  • 6th February, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Macedonia, the Republic of Macedonia, the Republic of
heretic

Comments and Discussions

 
Questionmore new? Pin
Pete Lomax Member 1066450510-Feb-20 19:57
professionalPete Lomax Member 1066450510-Feb-20 19:57 
AnswerRe: more new? Pin
Martin ISDN12-Feb-20 23:37
Martin ISDN12-Feb-20 23:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.