The problem
Your code isn't working the way you want because you are mixing async flow with sync flow.
When you call .then()
, it will return this
synchronously. Since setTimeout()
is an async function that is called after some time (2 secs), this.value
is still null
.
If you want to know more about the asynchronous flow of JS, I recommend watching this video. It's a bit long but super helpful.
Making your code work
Since we can't tell when setTimeout()
will call the function passed to it, we cannot call and callback functions that depend on its operations. We store these callbacks in an array for later consumption.
When the setTimeout()
function is called (promise resolves), we have the result of promise resolution. And so, we now call all the bound callbacks.
class APromise {
constructor(Fn) {
this.value = null;
- Fn(resolved => { this.value = resolved; });
+ this.callbacks = [];
+ Fn(resolved => {
+ this.value = resolved;
+
+ this.callbacks.forEach(cb => {
+ cb(this.value);
+ });
+ });
}
then(fn) {
- fn(this.value);
+ this.callbacks.push(fn);
return this;
}
}
function myFun() {
return new APromise(resolve => {
setTimeout(() => { resolve('Hello'); }, 2000);
});
}
const log = v => { console.log(v); };
myFun().then(log).then(log);
The chaining problem
The code above solves the problem partially.
True chaining is achieved when the result of one callback is passed on to the next. That's not the case in our current code. TO make that happen, each .then(cb)
must return a new APromise
that resolves when the cb
function is called.
A complete and Promises/A+ conformant implementation is way over the scope of a single SO answer, but that shouldn't give the impression that it isn't doable. Here's a curated list of custom implentations.
Fuller implementation
Let's start with a clean slate. We need a class Promise
that implements a method then
which also returns a promise to allow chaining.
class Promise {
constructor(main) {
// ...
}
then(cb) {
// ...
}
}
Here, main
is a function that takes a function as an argument and calls it when the promise is resolved/fulfilled - we call this method resolve()
. The said function resolve()
is implemented and provided by our Promise
class.
function main(resolve) {
// ...
resolve(/* resolve value */);
}
The basic feature of the then()
method is to trigger/activate the provided callback function cb()
with the promise value, once the promise fulfills.
Taking these 2 things into account, we can rewire our Promise
class.
class Promise {
constructor(main) {
this.value = undefined;
this.callbacks = [];
const resolve = resolveValue => {
this.value = resolveValue;
this.triggerCallbacks();
};
main(resolve);
}
then(cb) {
this.callbacks.push(cb);
}
triggerCallbacks() {
this.callbacks.forEach(cb => {
cb(this.value);
});
}
}
We can test our current code with a tester()
function.
(function tester() {
const p = new Promise(resolve => {
setTimeout(() => resolve(123), 1000);
});
const p1 = p.then(x => console.log(x));
const p2 = p.then(x => setTimeout(() => console.log(x), 1000));
})();
// 123 <delayed by 1 second>
// 123 <delayed by 1 more second>
This concludes our base. We can now implement chaining. The biggest problem we face is the the then()
method must return a promise synchronously which will be resolved asynchronously.
We need to wait for the parent promise to resolve before we can resolve the next promise. This implies that instead of adding cb()
to parent promise, we must add the resolve()
method of next promise which uses return value of cb()
as its resolveValue
.
then(cb) {
- this.callbacks.push(cb);
+ const next = new Promise(resolve => {
+ this.callbacks.push(x => resolve(cb(x)));
+ });
+
+ return next;
}
If this last bit confuses you, here are some pointers:
Promise
constructor takes in a function main()
as an argument
main()
takes a function resolve()
as an argument
resolve()
is provided by the Promise
constructor
resolve()
takes an argument of any type as the resolveValue
Demo
class Promise {
constructor(main) {
this.value = undefined;
this.callbacks = [];
const resolve = resolveValue => {
this.value = resolveValue;
this.triggerCallbacks();
};
main(resolve);
}
then(cb) {
const next = new Promise(resolve => {
this.callbacks.push(x => resolve(cb(x)));
});
return next;
}
triggerCallbacks() {
this.callbacks.forEach(cb => {
cb(this.value);
});
}
}
(function tester() {
const p = new Promise(resolve => {
setTimeout(() => resolve(123), 1000);
});
const p1 = p.then(x => console.log(x));
const p2 = p.then(x => setTimeout(() => console.log(x), 1000));
const p3 = p2.then(x => setTimeout(() => console.log(x), 100));
const p4 = p.then((x) => new Promise(resolve => {
setTimeout(() => resolve(x), 1000);
}))
/*
p: resolve after (1s) with resolveValue = 123
p1: resolve after (0s) after p resolved with resolveValue = undefined
p2: resolve after (0s) after p resolved with resolveValue = timeoutID
p3: resolve after (0s) after p2 resolved with resolveValue = timeoutID
p4: resolve after (1s) after p resolved with resolveValue = Promise instance
*/
})();
// 123 <delayed by 1s>
// 2 <delayed by 1.1s>
// 123 <delayed by 2s>