Promises Cookbook

A list of different snippets of code to solve problems that are frequently encountered with Promises.

Timing out.

Let’s start slow with a Promise that’s resolved after a certain time has passed.

function timeoutPromise(msec) {
  return new Promise(function(resolve) {
    setTimeout(resolve, msec);
  });
}

Retry-ing

This recipe will call a function fn (that returns a promise) up to nbTimes and resolve/reject depending on the result. If fn returns a resolved promise then the value is passed down, else we try to call it again or pass down the rejection reason.

function retryPromise(fn, nbTimes) {
  return fn()
    .catch(function _catch(err) {
      nbTimes--;
      if (nbTimes >= 0) {
        return fn().catch(_catch);
      }
      else {
        throw err;
      }
    });
}

Timing out

This recipe will call a function and reject its promise automatically if it hasn’t finished after a number of milliseconds.

It uses the fact that a promise cannot be resolved/rejected more than once (all other calls will be discarded silently).

function rejectAfter(fn, msec) {
  return new Promise(function(resolve, reject) {
    fn().then(resolve, reject);
    setTimeout(function() {
      reject(new TimeoutError());
    }, msec);
  });
}

Catch-if

This recipe will change the prototype of Promises to add a catchIf method that calls the resolver only if the error is an instanceof the argument. If not, it will keep on rejecting.

Promise.prototype.catchIf = function catchIf(Klass, handler) {
  return this.catch(function(err) {
    if (err instanceof Klass) {
      return handler(err);
    }
    else {
      return Promise.reject(err);
    }
  });
};

You can use it like this:

functionThatReturnsAPromise()
  .catchIf(SyntaxError, function(err) {
    // Silently ignore syntax errors.
  })
  .catchIf(ReferenceError, function(err) {
    console.error(err);
  })
  // Maybe other catchIf().
  ;

Finally

The standard promise does not include a finally method that is run in both rejections and resolutions. Use this recipe to add it. It will keep on resolving/rejecting.

Promise.prototype['finally'] = function _finally(fn) {
  return this.then(function(value) {
    fn();
    return value;
  }, function(err) {
    fn();
    throw err;
  });
};

Logging

This recipe will call a function fn and return the promise that it resolves, but will also console.log its output, without messing the chain. Pretty straightforward with branching.

function logResult(fn) {
  var p = fn();
  p.then(function(result) {
    console.log(result);
    return result;
  }, function(err) {
    console.error(err);
    throw err;
  });
  return p;
}

Resolve rejections

Use this recipe when you want your rejections to be passed in as resolutions.

function alwaysResolve(fn) {
  return new Promise(function(resolve) {
    try {
      fn().then(resolve, resolve);
    }
    catch(ex) {
      resolve(ex);
    }
  });
}

Safekeeping reject, resolve ( ES6 only )

Just a simple shorthand function to get the resolve/reject of a promise without too much code.

function makePromise() {
  let resolve, reject;
  let p = new Promise((rs, rj) => { resolve = rs; reject = rj; });
  return [p, resolve, reject];
}

You can use it like this:

let [p, resolve, reject] = makePromise();

We use this a lot in our source for keeping a map of resolve/reject for certain API calls.

Loops in promises

This is a more complex recipe that calls a function with each element of a loop. If the function returns a Thenable, it will wait until it resolves before calling it on the next element of the loop.

function forEachPromise(array, fn) {
  return new Promise(function(resolve, reject) {
    var p;
    array.forEach(function(elem, i) {
      if (p) {
        p = p.then(function() { return fn(elem, i); });
      }
      else {
        var maybeP = fn(elem, i);
        if (maybeP && 'then' in maybeP) {
          p = Promise.resolve().then(function() { return maybeP; });
        }
      }
    });
    if (!p) {
      resolve();
    }
    else {
      p.then(resolve, reject);
    }
  });
}

The reason for the complexity is that in the case where fn() does not return anything, we want to execute the next element right away and keep the stack. If, on the other hand, an exception happens or a rejection happens, we want to pipe the rest through and let the Promise take care of rejecting properly (calling the catches). This function will always return a valid Promise.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>