/thenfail

Just another Promises/A+ implementation written in TypeScript.

Primary LanguageTypeScript

NPM Package Build Status

Promises/A+ logo

ThenFail v0.4

Just another Promises/A+ implementation written in TypeScript that provides control flow tools like Promise.break, Promise.goto and disposable context.

Documentation

https://vilic.github.io/thenfail/doc

What is promise

Promise is a pattern for handling asynchronous operations.

The most obvious benifit you may get from promise itself IMO is on error handling, and maybe the ability to await an async operation in the future (ES7 and TypeScript 1.6/2.0).

Install ThenFail

npm install thenfail --save

If you are using TypeScript, ThenFail requires version 1.6 to compile correctly.

Create a promise from the scratch

My personal promise experience began with Q (a popular Promises/A+ implementation). But believe it or not, I spent a lot of time just for fully understanding how to actually create a promise.

So let's put the beginning at the beginning (unlike where Q put it), how to create the very first promise with ThenFail?

Firstly, let's import ThenFail:

import Promise from 'thenfail';

Then let's follow the ES6 standard:

let firstPromise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('biu'), 100);
});

And now we've created a promise that will be fulfilled with string "biu" after 100 ms.

Which means if we add the then method (which you see everywhere) with console.log, you will be able to see the output after 100 ms:

// Logs "biu" in 100 ms.
let secondPromise = firstPromise.then(result => console.log(result));

And... Wait, we get the secondPromise? Yes, every time we call then with handler(s), it creates another promise. The newly created promise will be fulfilled after the task in the handler gets done.

But how does it determine whether the task is done?

The method then generally accepts two parameters, an onfulfilled handler and an onrejected handler. Only one of the two handlers would ever be called (if both of them exist) depending on the state of the previous promise. And when been called, it returns a normal value or a promise, or otherwise, throws an error.

If the handler does not return a promise, like in the previous example (which returns undefined), the created promise (as then method will create a promise any way) will be switched to state fulfilled (or rejected if the handler throws an error).

Otherwise, the state of the created promise will be determined by the promise returned in the handler (which might take some time to be settled, asynchronously). And if there's a following then, the handlers in it will be triggered later depending on the returned promise.

Create a ThenFail promise chain

We've learned how to use the constructor to create a promise, but usually we only use that way to bridge other style of asynchronous code. When we are writting our own promised API based on other promised API, we can make it simpler:

function getResource(url: string): Promise<Resource> {
    if (url) {
        return Promise
            .then(() => {
                if (url in cache) {
                    return cache[url];
                } else {
                    return fetchRemoteRawResource(url);
                }
                
                // And you can certainly throw an error here.
                // It will be caught by the promise chain and trigger the closest `onrejected` handler.
            })
            .then(rawResource => processResource(rawResource));
    } else {
        // Never return `undefined` directly in an API that's meant to return a promise.
        // In ThenFail, you may use something like this.
        return Promise.void;
    }
}

getResource('the-resouce-url').then(resource => {
    if (resource) {
        // ...
    }
});

Promise works great with other promises. There are two points in this sentense:

  1. Promise works great in a promise-based architecture.
  2. A promise implementation usually plays well with other promise implementations. (Or it will most likely fail the Promises/A+ test suite.)

But there's a simple issue. Different implementations of promise have different APIs, some libraries you need may use Q, and some others might use Bluebird. Even the versions could vary. So when writing your own modules, you may want to unify the promises.

Assuming you are getting a promise (or a normal value if it might be), you may use the standard resolve method:

// A thenable is a promise like object (or a promise by another implementation).
function foo(bar: Thenable<string>|string): Promise<number> {
    return Promise
        .resolve(bar)
        .then(str => str.length);
}

After resolve, the promise or value will be converted to a ThenFail promise (if it's not). This will help preventing some undesired behaviors or errors from happening.

Control flow with ThenFail

Promise.break

Sometimes chaining promises could be annoying. For example:

Promise
    .then(() => {
        // step 1
    })
    .then(() => {
        // step 2
        if (noNeedToContinue) {
            // How to break here?
        }
    })
    .then(() => {
        // step 3.1
    }, reason => {
        // step 3.2
    })
    .then(() => {
        // step 4
    });

Now it's easy with ThenFail:

Promise
    .then(() => {
        // step 1
    })
    .then(() => {
        // step 2
        if (noNeedToContinue) {
            Promise.break;
        }
    })
    .then(() => {
        // step 3.1
    }, reason => {
        // step 3.2
    })
    .then(() => {
        // step 4
    })
    // enclose current context so it won't break too many.
    // it should be required if this could be directly chained somewhere else.
    // e.g. Returned as your method result.
    .enclose();

Promise.goto

Promise
    .then(() => {
        if (someCondition) {
            Promise.goto('label-a');
        } else {
            Promise.goto('label-b');
        }
    })
    .then(() => {
        // will not be called.
    })
    .label('label-a', () => {
        // step 3.1
    }, reason => {
        // step 3.2
    })
    .label('label-b', () => {
        // step 4
        // be aware that `goto` `"label-a"` won't prevent the execution of `"label-b"`.
    });

Promise context

There's situations we may want to cancel a promise chain, not only from inside (like using Promise.break), but also from outside:

page.on('load', () => {
    Promise
        .then(() => {
            // do some works...
        })
        .then(() => {
            // more works...
        })
        .then(() => {
            // ...
        });
});

page.on('unload', () => {
    // how can we cancel the promise chain?
});

With ThenFail, every promise has something called context. And if the context is disposed, the chain gets cancelled.

import { Context } from 'thenfail';

let context: Context;

page.on('load', () => {
    let promise = Promise
        .then(() => {
            // do some works...
        })
        .then(() => {
            // more works...
        })
        .then(() => {
            // ...
        });
    
    context = promise.context;
});

page.on('unload', () => {
    // dispose the context.
    if (context) {
        context.dispose();
        context = undefined;
    }
});

As what you might guess, The enclose method we mentioned before can mark current context as enclosed, and no longer accept new promises being under the same context. Which means a new context will be created when the then method is called.

Notable basics

States

Promises/A+ defines 3 states of a promise: pending, fulfilled and rejected. And only the pending state may transform to other states.

In the implementation of ThenFail, one more state skipped is added to specify the state of promises skipped by (fake) break, goto or a disposed context (However, the promise of which onfulfilled or onrejected has been called will not be skipped).

Resolve

Resolve is a process that may change the state of promise from pending to either fulfilled or rejected (not just the former state).

Terms

Promise

A promise in the documentation means the implementation of Promises/A+ in ThenFail.

Thenable/PromiseLike

A thenable or promise-like in the documentation means any other implementation that might act somewhat or exactly as we may expect a Promises/A or Promises/A+ implementation will.

Resolvable

A resolvable in the documentation means a thenable/promise-like or a normal value.

License

MIT License.