Search code examples
javascriptpromisees6-promisefail-fast

Implementing a fail-fast design with promises in JavaScript


I'm not sure if "fail-fast" is the best way to describe this methodology, but ever since I started to learn about programming I have always been taught to design functions like this:

function doSomething() {
    ... // do error-prone work here

    if (!allGood) {
        // Report error, cleanup and return immediately. Makes for cleaner,
        // clearer code where error-handling is easily seen at the top
        ...
        return;
    }

    // Success! Continue on with (potentially long and ugly) code that may distract from the error
}

As such, I'm trying to call a promisified function like so:

doSomethingAsync(param).catch(err => {
    console.error(err);
}).then(() => {
    // Continue on with the rest of the code
});

But this gives me behaviour akin to the finally block of a classic try...catch...finally statement, i.e. the then() block will always be called, even after an error. Sometimes this is useful, but I rarely find myself needing such functionality (or try...catch statements in general, for that matter).

So in the interest of failing as quickly and clearly as possible, is there a way that I can make the second example above work in the way that I expect (i.e. then() is only executed if catch() wasn't, yet a single catch() will still catch all errors raised by doSomethingAsync())?


Solution

  • If you use async and await instead of .then, you can effectively wait for the Promise to resolve (or reject), and if it rejects, return early:

    (async () => {
      try {
        await doSomethingAsync(param);
      } catch(err) {
        console.error(err);
        return;
      }
      // Continue on with the rest of the code
    })();
    

    const doSomethingAsync = () => new Promise((resolve, reject) => Math.random() < 0.5 ? resolve() : reject('bad'));
    
    (async () => {
      try {
        await doSomethingAsync();
      } catch(err) {
        console.error(err);
        return;
      }
      console.log('continuing');
    })();

    That's what I'd prefer. You can also use the .then(onResolve, onReject) technique, though it's usually not recommended:

    function onReject(err) {
      console.log(err);
    };
    doSomethingAsync(param).then(onResolve, onReject);
    function onResolve() {
      // Continue on with the rest of the code
    }
    

    const doSomethingAsync = () => new Promise((resolve, reject) => Math.random() < 0.5 ? resolve() : reject('bad'));
    
    function onReject(err) {
      console.log(err);
    };
    doSomethingAsync().then(onResolve, onReject);
    function onResolve() {
      console.log('continuing');
    }

    This will have onReject only handle errors thrown by doSomethingAsync(param). If your onResolve can throw inside its body as well, then you'll have to chain another .catch onto it (which will start to look a bit messy - it's usually nicer to catch errors in just one place)