Recreating Promises in Javascript

|
Lucknow, India
|
10 minutes

The Promise class is one of the most notoriously black-boxed tools in Javascript. How does the promise class manage asynchronicity, when Javascript is single-threaded?

I made a small Promise class to try to understand. The complete implementation is at the end of this article.

This article assumes some familiarity with the concept of Promises. If you’re unfamiliar with Promises, I’d highly consider checking out this wonderful guide.

The Specification#

To focus on core concepts, we’ll assume that our Promises always resolve and return a value. In other words, we won’t be handling rejections and errors.

We of course want to be able to create a Promise object:

let promise = new Promise((res, rej) => setTimeout(() => res('done'), 1000));

We also want to be able to act on that Promise object, using the then function. We’ll restrict our then to fulfilled Promises only (accepting one parameter).

let res = (val) => alert(val);
promise.then(res);

Finally, we want to be able to chain thens together, to act on each of its predecessor’s values.

promise.then(res).then(res).then(res);

The Skeleton#

The bare bones:

class Promise {
    constructor(executor) {
        // do something
    }

    then(onFulfilled) {
        // do something
    }
}

Let’s also write the reject and resolve function, which will be passed into the executor function.

class Promise {
    constructor(executor) {
        ...
        this.outcome = null;
        this.reject = this.reject.bind(this);
        this.resolve = this.resolve.bind(this);

        executor(this.resolve, this.reject);
        ...
    }

    resolve(value) {
        this.outcome = {status: 'fulfilled', value: value};
    }

    reject(error) {
        this.outcome = {status: 'rejected', reason: error};
    }
    ...
}

Either reject or resolve will be called in the body of the executor. The called function will update the state of the Promise object by changing the value of outcome.

We are achieving asynchronicity through callback functions.

The then Function#

The then function accepts a function, and must always return a Promise. A naive version:

class Promise {
    ...
    then(onFulfilled) {
        return new Promise((res, rej) => {
            onFulfilled(this.outcome.value);
        });
    }
    ...
}

The problem: if the then function is called, and the Promise is not settled, this.outcome will be null, and would cause an error. We may be able to manage this by checking if outcome==null. If it is null, we can set a wait time, after which we will check again.

class Promise {
    ...
    then(onFulfilled) {
        if (this.outcome === null) {
            setTimeout(() => then(onFulfilled), 10);
        }
        else {
            return new Promise((res, rej) => onFulfilled(this.outcome.value));
        }

    }
    ...
}

However, if this.outcome == null, we will end up with no return statement and the return value will be undefined. This breaks the specification we gave our then function: > The then function must always return a Promise.

The Catch 22#

The then function cannot create a Promise until the previous Promise has been settled. Thus, it has to wait.

The then function always has to return a Promise when it is called. Thus, it cannot wait.

A new Promise cannot be created until the previous one has been settled, but a Promise has to be returned. This leaves us only one choice.

Return the Promise object we already have.

class Promise {
    ...
    then(onFulfilled) {
        return this;
    }
    ...
}

The then function will usually be executed before the Promise settles. So, we can simply store the onFulfilled function in an instance variable and call for it on a later date.

class Promise {
    constructor(executor) {
        ...
        this.thenFunction = null;
    }
    ...
    then(onFulfilled) {
        this.thenFunction = onFulfilled;
        return this;
    }
    ...
}

We want to execute this.thenFunction once the Promise has settled – that is, whenever either resolve or reject are called. We also want to update the state of the Promise object to account for any future calls.

class Promise {
    resolve(value) {
        ...
        this.callThen();
    }

    reject(error) {
        ...
        this.callThen();
    }
    ...

    callThen() {
        if (this.thenFunction)
            this.outcome.value = this.thenFunction(this.outcome.value);
    }
    ...
}

Finally, to account for a then function call after the Promise has been resolved.

class Promises {
    then(onFulfilled) {
        this.thenFunction = onFulfilled;
        if (this.outcome !== null) {
            this.callThen();
        }
        return this;
    }
}

then Chaining#

Oone last thing to handle – chaining thens. The problem with our current implementation is that, each time then is called, this.thenFunction gets overwritten. To handle this, we change our thenFunction to an array.

class Promise {
    constructor(executor) {
        ...
        this.thenFunctions = [];
        ...
    }
    ...
    then(onFulfilled) {
        ...
        this.thenFunctions.push(onFulfilled);
        ...
    }
    ...
    callThen() {
        if (this.thenFunctions) {
            this.thenFunctions.forEach(fn => {
                this.outcome.value = fn(this.outcome.value)
            });
            this.thenFunctions = [];
        }
    }
    ...
}

And we’re done!

Bringing It All Together#

Here is our complete Promise implementation:

class Promise {
    constructor(executor) {
        this.outcome = null;
        this.thenFunctions = callStack;

        this.resolve = this.resolve.bind(this);
        this.reject = this.reject.bind(this);

        executor(this.resolve, this.reject);
    }

    resolve(value) {
        this.outcome = {status: 'fulfilled', value: value};
        this.callThen();
    }

    reject(error) {
        this.outcome = {status: 'rejected', reason: error};
        this.callThen();
    }

    then(onFulfilled) {
        this.thenFunctions.push(onFulfilled);
        if (this.outcome !== null) {
            this.callThen();
        }
        return this;
    }

    callThen() {
        if (this.thenFunctions) {
            this.thenFunctions.forEach(fn => {
                this.outcome.value = fn(this.outcome.value)
            });
            this.thenFunctions = [];
        }
    }

}

The Hidden Flaws#

There’s a tiny lie that this execution of the Promise class tells its user. When then is called, the user assumes that it returns a new Promise. This is not the case. It is always the original Promise object that is returned. It is also the original Promise that is modified to reflect the then calls.

Because of this, parellel then calls will not work as intended:

// let promise = <Promise object>

promise.then((val) => val+"2");
promise.then((val) => val+ "3");

The second then function should return value3, but would return value23. The fix for this would be to distinguish then chaining and parellel thens, which this implementation does not do.

If anyone is interested in writing an extension to this implementation, incorporating rejections and fixing other faults, feel free to send me a pull request on the repo.

If you enjoyed reading this, please consider signing up to my newsletter.