Break or Cancel the Promises Chain

Promise

Promise has become an important tool for managing asynchronous operations in JavaScript. However, sometimes we would still find promises kind of annoying.

Promise
    // Here `then` is an equivalence to `Promise.resolve().then`.
    .then(() => {
        // Start.
    })
    .then(() => {
        if (wantToBreakHere) {
            // How?
        }
    })
    .then(() => {
        // Something to skip.
    });

We may nest the following promise, but that will be painful if it's a long long chain.

Luckily we have a partial workaround for many promise implementations (as most of them have the helper catch):

class BreakSignal { }

Promise
    .then(() => {
        // Start.
    })
    .then(() => {
        if (wantToBreakHere) {
            throw new BreakSignal();
        }
    })
    .then(() => {
        // Something to skip.
    })
    .catch(BreakSignal, () => {
        // Use catch method to filter BreakSignal.
    });

This approach is somewhat gentle, but if you have other catches (rejection handlers) in this chain, you will have to relay BreakSignal manually.

Consider another situation than "break":

page.on('load', () => {
    Promise
        .then(() => asyncMethodA())
        .then(result => asyncMethodB(result))
        .then(result => {
            // Update something...
        });
});

If the load event fires twice in a short time, we'll need to cancel the first promises chain, or at least to prevent it from updating related data or view (If this is the desired behavior).

Again, the workaround:

class BreakSignal { }

let context;

function createWrapper() {
    let currentContext = context;
    
    return function (handler) {
        return function () {
            if (context !== currentContext) {
                throw new BreakSignal();
            }
            
            return handler.apply(undefined, arguments);
        };
    };
}

page.on('unload', () => {
    context = undefined;
});

page.on('load', () => {
    context = {};
    let wrap = createWrapper();

    Promise
        .then(wrap(asyncMethodA))
        .then(wrap(asyncMethodB))
        .then(result => {
            // Update something...
        })
        .catch(BreakSignal, () => { });
});

ThenFail

After these mess, I come up adding the ability to break or cancel a promises chain in my own promise implementation ThenFail.

Promise
    .then(() => {
        // Start.
    })
    .then(() => {
        if (wantToBreakHere) {
            Promise.break;
        }
        
        return Promise
            .then(() => {
                Promise.break;
            })
            .then(() => {
                // Never reaches here.
            });
            // No need to enclose nested context.
    })
    .then(() => {
        // Something to skip.
    })
    // Enclose current context to prevent from breaking too many.
    // This is important if the promise chain might be used by code out of your control.
    .enclose();

Actually you may also "break" each of ThenFail:

Promise
    .each([1, 2, 3], value => {
        if (value > 1) {
            Promise.break;
            
            // Or asynchronously:
            return Promise
                .then(() => {
                    // Do some work.
                })
                .break;
        }
    })
    .then(completed => {
        if (completed) {
            // Some code...
        }
    });

I've mentioned the concept of context in ThenFail. And to cancel an entire promises chain (of the same context), you just need to dispose the context.

let context;

page.on('unload', () => {
    context.dispose();
});

page.on('load', () => {
    let promise = Promise
        .then(() => asyncMethodA())
        .then(result => asyncMethodB(result))
        .then(result => {
            // Update something...
        });
    
    context = promise.context;
});

BTW, disposing a context will also dispose nested contexts. :D