Write before the text

Article from Maciej Cieślar: Implementing Promises In JavaScript. This article describes how the author tried to implement TypeScript Promises after he learned how to use Promises. If the translation of this article is not good, please forgive me. If you have any suggestions, please leave them in the comments section below.

implementing promises in JavaScript

One of my favorite moments in programming is when you get to the point where you completely understand a concept and ah, that’s it. Even though the process may take a lot of time and effort, happiness will be worth it when it comes.

I think the most effective way to assess (and help improve) our understanding of a topic is to try and put that knowledge to work. Doing so not only allows us to recognize and ultimately address our weaknesses, but also gives us some insight into how things work. Even a simple trial-and-error approach can reveal details that were often overlooked before.

With this in mind, I think learning how to make Promises was one of the most important moments of my programming career. It gave me a different way to understand how asynchronous code works and made me a better programmer.

I hope this article has helped you implement your own promises in JavaScript over time.

We will focus on implementing Promises according to Promises/A+ specifications and BluebirdApi methods. Jest practice test drivers are also used.

TypeScript also comes in handy. Since we’re going to go crazy here, I’m going to assume that you have a basic understanding of promises and a vague idea of how they work. If you don’t, this is certainly a good place to start.

Now that we have the direction, let’s get down to business. Clone branches first, and then we can start together.

The core of Promise

As we all know, a promise is an object that has the following properties:

Then

A way to add a processor to our promise. It will return a new Promise containing the value passed from the method in the previous handler.

Handlers processor

The array of processors will be appended to the THEN. The handler is an object that has two methods, onSuccess and onFail, which are passed as arguments to then(onSuccess,onFail).

The interface code of the processor is as follows:

type HandlerOnSuccess<T, U = any> = (value: T) = > U | Thenable<U>;
type HandlerOnFail<U = any> = (reason: any) = > U | Thenable<U>;

interface Handler<T, U> {
    onSuccess: HandlerOnSuccess<T, U>;
    onFail: HandlerOnFail<U>;
}
Copy the code

State of the State

A promise there will be one of three states: resolved, the rejected and pending.

Resolved means either things are going smoothly and we’ve received a value, or we’ve caught and handled our error.

Rejected means that either our request is Rejected or our error is thrown but not caught.

Pending means that neither resolve nor Rejected is currently called, and we are still waiting for that value.

There’s a term called promise resolved which means a promise is either in resolved or rejected.

The Value Value

The value is either Rejected or Resolved. Once the value is set, it cannot be changed.

test

With the TDD approach (test-driven), we need to write the test code before we write the real code. Here is a test case for our core code:

describe('PQ <constructor>'.(a)= >{
    / / is a promise
    test('resolves like a promise'.(a)= > {
        return new PQ<number>((resolve) => {
            setTimeout(() => {
                resolve(1); 30},);
        }).then((val) => {
            expect(val).toBe(1);
        }); }); // Always asynchronoustest('is always asynchronous', () = > {const p = new PQ((resolve) => resolve(5));
            expect((p as any).value).not.toBe(5);
    }) / /resolveI can get the desired valuetest('resolves with the expected value',() = > {return new PQ<number> ((resolve) => 
            resolve(30)).then((val) => { expect(val).toBe(30);
        });
    }); / / in the callthenBefore,resolveathenabledObject / /"thenable"Is definedthenMethod object.test('resolves a thenable before calling then', () = > {return new PQ<number> ((resolve)=> 
            resolve(new PQ((resolve) => resolve(30))),
        ).then((val) => 
            expect(val).toBe(30));
    }) // Can catchrejectCase of errortest('catches errors(reject) ',) = > {const error = new Error('Hello there');
        return new PQ((resolve,reject) = > {return reject(error);
        }).catch((err: Error) => {
            expect(err).toBe(error)})}) // Can catch errors when an exception is throwntest('catches errors (throw) ',) = > {const error = new Error('General Kenobi! ');
        return new PQ(() = > {throw error;
        }).catch((err) => {
            expect(err).toBe(error); 
        });
    });
    
    //promiseIs immutable and can return a new onepromise
    test('is not mutable - then returns a new promise', () = > {const start = new PQ<number> ((resolve) => resolve(20));
        return PQ.all([
            start.then((val)=>{
                expect(val).toBe(20);
                return 30;
            }).then((val) => expect(val).toBe(30)), 
            start.then(val => expect(val).toBe(20)),])})})Copy the code

Run our tests

I strongly recommend using the Jest plug-in in Visual Studio Code. It can run our tests in the background and display the results directly in lines of code, green for passing, red for failing, and so on.

We can also see the results of the run through the Output console and select the JEST tag.

There is another way to run tests.

npm run test
Copy the code

No matter how we run the tests, we can see that all the tests fail.

So let’s make them pass now.

Implementing core Promises

The constructor

class PQ<T> { private state: States = States.PENDING; private handlers: Handler<T, any>[] = []; private value: T | any; public static errors = errors; public constructor(callback: (resolve: Resolve<T>, reject: Reject) => void) { try { callback(this.resolve, this.reject); } catch (e) { this.reject(e); }}}Copy the code

Our constructor takes a callback function as an argument.

Resolve and this.reject are used as arguments when we call this callback.

Note that normally we would bind this.resolve and this.reject to this, but we use the class arrow function instead.

Set the results

Now we need to set the result. Remember that we must handle the result correctly, which means that if we want to return a promise, we must resolve it first.

Class PQ<T> {
   / /...
   
   private setResult = ( value : T | any, state: States) = > {
        const set = (a)= >{
            if( this.state ! == States.Pending){return null
            }
            
            if( isThenable(value)) {
                return ( value as Thenable <T>).then(this.resolve , this.reject);
            }
            
            this.value = value;
            this.state = state;
            
            return this.executeHandlers();
       };
   setTimeout( set , 0);
};
Copy the code

First, we check to see if the state is not pending – if it is, then the promise has already been processed and we cannot assign any new values to it.

Second, we check if the value is a Thenable object (an object with a THEN). Simply put, thenable is an object with a THEN method.

By convention, an object with a THEN should behave like a promise. So to get the correct result, we’ll call then and pass this.resolve and this.reject as arguments.

Once the “object with THEN” is set up, it will call one of our methods and give the non-promise value we expect.

So now we need to check whether the object is an object with then.

Test for objects that have THEN:

describe('isThenable', () = > {test('detects objects with a then method', () => {
        expect(isThenable({ then: () => null })).toBe(true);
        expect(isThenable(null)).toBe(false);
        expect(isThenable({})).toBe(false);
     });
});
Copy the code

The isThenable method code:

const isFunction = (func: any) => 
    typeof func === 'function';

const isObject =  (supposedObject: any) =>
    typeof supposedObject === 'object'&& supposedObject ! == null && ! Array.isArray(supposedObject); const isThenable = (obj: any) => isObject(obj) && isFunction(obj.then);Copy the code

It’s worth noting that even if the internal code of the callback function is synchronized, our promise is never synchronized.

We use setTimeout to delay execution until the next round of the event loop begins.

Now all we need to do is set our value and status values, and then execute the handler we’ve already written.

Execution processor

    Class PQ<T> {
        // ...
        
        private executeHandlers = () => {
            if(this.state === State.pending){
                return null
            }
            
            this.handlers.forEach((handler) => {
                if (this.state === States.REJECTED) {
                    return handler.onFail(this.value);
                }
                return handler.onSuccess(this.value);
           })
           
           this.handler = [];
        };
    }
Copy the code

Again, the state cannot be pending.

The state of the promise determines the function we will call. If it is Resolved we will call onSuccess, otherwise we will call onFail.

To be safe, let’s now clear out our processor array to prevent any unexpected operations in the future. Because the handler will be executed after it is added.

That’s what we’ll have to talk about next: the way we add our processors.

AttachHandler Adds a processor

private attachHandler = (handler: Handler<T , any>) => {
    this.handlers = [ ... this.handlers,handler];
    this.execureHandlers();
};
Copy the code

As you can see, we simply add a new handler to our processor array and execute the handler. That’s all.

Now, let’s put them together to implement our THEN method.

then

Class PQ<T> {
    public then<U>( onSuccess? :HandlerOnSuccess<T , U>,onFail? : HandlerOnFail ) {return new PQ< U | T >((resolve, reject) => {
            return this.attachHandler({
            onSuccess: (result) => {
                if(! onSuccess) {return resolve(result);
                }
                
                try{
                    return resolve(onSuccess(result));
                }
                catch (e){
                    return reject(e);
                }
            },
            onFail: (reason) =>{
                if(! onFail){return reject(reason);
                }
                
                try{
                    return resolve(onFail(reason));
                }
                catch(e){
                    returnreject(e); }}})})}}Copy the code

In then, we need to return a promise, and in the callback we need to add a handler that will wait for the current promise to be processed.

When this happens, whether the onSuccess handler or the onFail handler is executed, we will proceed accordingly.

One thing to remember is that you don’t have to pass the processor to then. This is important, but it’s equally important that we don’t try to implement anything undefined.

Also, when the handler is passed to onFail, we actually resolve the returned promise because the thrown error has already been caught.

catch

Catch is really an abstraction of the THEN method.

public catch<U>(onFail: HandlerOnFail<U>) {
    return this.then<U>(identity, onFail);
 }
Copy the code

That’s all.

Finally

Finally is also an abstraction of then (finallyCb, finallyCb), because we don’t really care about the outcome of promises.

In fact, he keeps the result of the previous promise and returns it. So the result returned by finallyCb doesn’t matter.

Test case for finally:


describe('PQ.prototype.finally', () => {
    test('it is called regardless of the promise state', () = > {let counter = 0
        ;return PQ.resolve(15)
        .finally((a)= > {
            counter += 1;
        }).then((a)= > {
            return PQ.reject(15);
        }).then((a)= > {
            // wont be called
            counter = 1000;
        }).finally((a)= > {
            counter += 1;
        }).catch((reason) = > {
            expect(reason).toBe(15);
            expect(counter).toBe(2);
        });
    });
});
Copy the code

Class PQ<T>{
    public finally<U>(cb: Finally<U>) {
        return new PQ<U>((resolve, reject) = > {let val: U | any;
            let isRejected: boolean;
            return this.then(
                (value) => {
                    isRejected = false;
                    val = value;
                    return cb(); },reason) => {
                    isRejected = true;
                    val = reason;
                    return cb();
                },
        ).then(
            () = > {if (isRejected) {
                    return reject(val);
                }
                return resolve(val);
            });
        }); }}Copy the code

toString

Test cases:

describe('PQ.prototype.toString', () => {
    test('return [object PQ]',() => {
        expect(new PQ<undefined>((resolve) = > resolve()).toString()).toBe(
            '[object PQ]',); }); });Copy the code

ToString implementation code

Class PQ<T>{
    public toString() {
        return`[object PQ]`; }}Copy the code

The toString function returns only one string [object PQ].

So far we have implemented the core methods of our Promise, and now we can implement some of the Bluebird methods mentioned earlier, which will make it easier for us to implement promises.

Additional method

Promise.resolve

How official documents work

Test cases:


describe('PQ.prototype.resolve', () => {
  test('resolves a value', () = > {return PQ.resolve(15).then((val) = > expect(val).toBe(15));
  });
});
Copy the code

Implementation code:


public staticresolve<U = any>(value? : U | Thenable<U>) {return new PQ<U>((resolve) = > {
      return resolve(value);
    });
  }
Copy the code

Promise.reject

How official documents work

The test case

describe('PQ.prototype.reject', () => {
  test('rejects a value', () = > {const error = new Error('Hello there');

    return PQ.reject(error).catch((err) = > expect(err).toBe(error));
  });
});
Copy the code

The implementation code

  public staticreject<U>(reason? : any) {return new PQ<U>((resolve, reject) = > {
      return reject(reason);
    });
  }
Copy the code

Promise.all

How official documents work

(Translator’s note: This API is different from the Promise native All.)

Test cases:

describe('PQ.all', () => {
  test('resolves a collection of promises', () = > {return PQ.all([PQ.resolve(1), PQ.resolve(2), 3]).then((collection) = > {
      expect(collection).toEqual([1.2.3]);
    });
  });

  test('rejects if one item rejects', () = > {return PQ.all([PQ.resolve(1), PQ.reject(2)]).catch((reason) = > {
      expect(reason).toBe(2);
    });
  });
});
Copy the code

Implementation code:

  public static all<U = any>(collection: (U | Thenable<U>)[]) {
    return new PQ<U[]>((resolve, reject) = > {if (!Array.isArray(collection)) {
        return reject(new TypeError('An array must be provided.'));
      }

      let counter = collection.length;
      const resolvedCollection: U[] = [];

      const tryResolve = (value: U, index: number) => {
        counter -= 1;
        resolvedCollection[index] = value;

        if (counter ! = = 0) {
          return null;
        }

        return resolve(resolvedCollection);
      };

      return collection.forEach((item, index) = > {return PQ.resolve(item)
          .then((value) = > {return tryResolve(value, index);
          }).catch(reject);
      });
    });
  }
Copy the code

I think the implementation is pretty simple.

Starting with collect.length, we decrease this value each time we run tryResolve until it reaches zero, at which point every task in the collection will have been resolved. Finally, we will resolve the newly created collection (each task is in resovle).

Promise.any

[principle] (bluebirdjs.com/docs/api/pr…

Test cases:

describe('PQ.any', () = > {test('resolves the first value', () = > {return PQ.any<number>([
      PQ.resolve(1),
      new PQ((resolve) => setTimeout(resolve, 15)),
    ]).then((val) => expect(val).toBe(1));
  });

  test('rejects if the first value rejects', () = > {return PQ.any([
      new PQ((resolve) => setTimeout(resolve, 15)),
      PQ.reject(1),
    ]).catch((reason) => {
      expect(reason).toBe(1);
    });
  });
});
Copy the code

Implementation code:

  public static any<U = any>(collection: (U | Thenable<U>)[]) {
    return new PQ<U>((resolve, reject) => {
      return collection.forEach((item) => {
        return PQ.resolve(item)
          .then(resolve)
          .catch(reject);
      });
    });
  }
Copy the code

We simply wait for the first value of resolve to come and return it in a promise.

Promise.props

How official documents work

Test cases:

describe('PQ.props', () => {
  test('resolves object correctly', () = > {return PQ.props<{ test: number; test2: number }>({
      test: PQ.resolve(1),
      test2: PQ.resolve(2),
    }).then((obj) = > {
      return expect(obj).toEqual({ test: 1.test2: 2 });
    });
  });

  test('rejects non objects', () = > {return PQ.props([]).catch((reason) = > {
      expect(reason).toBeInstanceOf(TypeError);
    });
  });
});
Copy the code

Implementation code:

 public static props<U = any>(obj: object) {
    return new PQ<U>((resolve, reject) = > {if (! isObject(obj)) {
        return reject(new TypeError('An object must be provided.'));
      }

      const resolvedObject = {};

      const keys = Object.keys(obj);
      const resolvedValues = PQ.all<string> (keys.map((key) => obj[key]));

      return resolvedValues
        .then((collection) = > {return collection.map((value, index) => {
            resolvedObject[keys[index]] = value;
          });
        })
        .then(() => resolve(resolvedObject as U)).catch(reject);
    });
  }
Copy the code

We iterate over the key passed into the object, resolve each value. We then assign the value to a new object, which will then be used to make the Promise become resolved.

Promise.prototype.spread

How official documents work

The test case

describe('PQ.protoype.spread', () = > {test('spreads arguments', () = > {returnPQ.all<number>([1, 2, 3]).spread((... args) => { expect(args).toEqual([1, 2, 3]);return 5;
    });
  });

  test('accepts normal value (non collection)', () = > {return PQ.resolve(1).spread((one) => {
      expect(one).toBe(1);
    });
  });
});
describe('PQ.spread', () = > {test('resolves and spreads collection', () = > {returnPQ.spread([PQ.resolve(1), 2, 3], (... args) => { expect(args).toEqual([1, 2, 3]); }); }); });Copy the code

Implementation code:

  public static spread<U extends any[]>(
    collection: U,
    handler: HandlerOnSuccess<any[]>,
  ) {
    return PQ.all(collection).spread(handler);
  }
Copy the code

Promise.delay

How official documents work

Test code:

describe('PQ.delay', () = > {// Wait the given number of milliseconds before resolve
  test('waits for the given amount of miliseconds before resolving', () = > {return new PQ<string>((resolve) = > {
      setTimeout((a)= > {
        resolve('timeout');
      }, 50);

      return PQ.delay(40).then((a)= > resolve('delay'));
    }).then((val) = > {
      expect(val).toBe('delay');
    });
  });

  test('waits for the given amount of miliseconds before resolving 2', () = > {return new PQ<string>((resolve) = > {
      setTimeout((a)= > {
        resolve('timeout');
      }, 50);

      return PQ.delay(60).then((a)= > resolve('delay'));
    }).then((val) = > {
      expect(val).toBe('timeout');
    });
  });
});
Copy the code

Implementation code:

public static delay(timeInMs: number) {
    return new PQ((resolve) = > {
      return setTimeout(resolve, timeInMs);
    });
  }
Copy the code

By using setTimeout, we can easily postpone the resolve operation for a given number of milliseconds.

Promise.prototype.timeout

How official documents work

The test code

describe('PQ.prototype.timeout', () => {
  test('rejects after given timeout', () = > {return new PQ<number>((resolve) = > {
      setTimeout(resolve, 50);
    })
      .timeout(40)
      .catch((reason) = > {
        expect(reason).toBeInstanceOf(PQ.errors.TimeoutError);
      });
  });

  test('resolves before given timeout', () = > {return new PQ<number>((resolve) = > {
      setTimeout((a)= > resolve(500), 500);
    })
      .timeout(600)
      .then((value) = > {
        expect(value).toBe(500);
      });
  });
});
Copy the code

Implementation code:

class PQ<T> {

  // ...
  
  public timeout(timeInMs: number) {
    return new PQ<T>((resolve, reject) = > {const timeoutCb = () = > {return reject(new PQ.errors.TimeoutError());
      };

      setTimeout(timeoutCb, timeInMs);

      return this.then(resolve);
    }); }}Copy the code

This is actually a bit of a problem.

If setTimeout executes faster than our promise, it will call our special error to reject the promise.

Promise.promisfy

How official documents work

The test case

describe('PQ.promisify', () => {
  test('works', () = > {const getName = (firstName, lastName, callback) = > {
      return callback(null.`${firstName} ${lastName}`);
    };

    const fn = PQ.promisify<string>(getName);
    const firstName = 'Maciej';
    const lastName = 'Cieslar';

    return fn(firstName, lastName).then((value) = > {
      return expect(value).toBe(`${firstName} ${lastName}`);
    });
  });
});
Copy the code

Implementation code:

 
  public static promisify<U = any>(
    fn: (. args: any[]) = > void,
    context = null,) {return (. args: any[]) = > {
      return new PQ<U>((resolve, reject) = > {
        return fn.apply(context, [
          ...args,
          (err: any, result: U) => {
            if (err) {
              return reject(err);
            }

            returnresolve(result); },]); }); }; }Copy the code

We bind all the passed arguments to the function, and – the last one – is our error-first callback function.

Promise.promisifyAll

How official documents work

Test code:

describe('PQ.promisifyAll', () => {
  test('promisifies a object', () = > {const person = {
      name: 'Maciej Cieslar',
      getName(callback) {
        return callback(null.this.name); }};const promisifiedPerson = PQ.promisifyAll<{
      getNameAsync: (a)= > PQ<string>;
    }>(person);

    return promisifiedPerson.getNameAsync().then((name) = > {
      expect(name).toBe('Maciej Cieslar');
    });
  });
});
Copy the code

Implementation code:

  public static promisifyAll<U>(obj: any): U {
    return Object.keys(obj).reduce((result, key) = > {
      let prop = obj[key];

      if (isFunction(prop)) {
        prop = PQ.promisify(prop, obj);
      }

      result[`${key}Async`] = prop;

      return result;
    }, {}) asU; }}Copy the code

We’ll iterate over the keys of the incoming object, promise its methods, and add the keyword async before each function name.

packaging

We’ve only implemented a small portion of all the BlueBird Api methods so far, so I strongly encourage you to explore, try calling, and then try implementing all the rest.

It may be difficult at first, but don’t get discouraged. After all, easy is meaningless.

Thank you very much for reading. I hope you found this article valuable and that it helped you understand the concept of promise once and for all. From now on you’ll feel good about using Promise or writing asynchronous code with it.

If you have any questions, please feel free to leave a comment or poke me in the message board below.

If you like me, follow my blog

Or subscribe to me.

Conclusion the translator

If you have any comments or suggestions on my translation or content, please leave a comment below and let me know. If you like this article, give a thumbs up. Thank you very much for reading it