Skip to content

Instantly share code, notes, and snippets.

@rstacruz
Last active December 7, 2019 21:08
Show Gist options
  • Save rstacruz/6964035 to your computer and use it in GitHub Desktop.
Save rstacruz/6964035 to your computer and use it in GitHub Desktop.
defer.js

Defer.js

Standardizes the interface for async APIs.

It helps you write great async API. It catches errors for you. It makes your async functions work with promises or callbacks.

When to use it

Protip: Any time you're writing a function that takes a callback, use defer.js. Yes. All of them. Why?

  • Ensures proper error propagation.
    No need for lots of try/catch blocks: those will be taken care of for you.

  • Support promises or callbacks.
    It makes your functions work with both async callbacks or promises with no extra code.

  • Portable.
    Works for Node.js and the browser. It's also pretty damn small (~70loc).

When not to use it: when your library does its async duties with 100% promises and doesn't work with anything that expects callbacks. q.js already features great error handling (q.try).

What it's not

  • It's not async.js, because that lets you work many async callback-functions in parallel (among other things).

  • It's not q.js or when.js or rsvp.js or promise.js, which helps you write promise functions and work with many promise objects. However, you can hook up defer.js to use any of those to generate promises.

Get started in 20 seconds

$ npm install rstacruz/defer

Then:

var defer = require('defer');
defer.promise = require('q').promise; /* <- optional */

Instead of writing an async function like so:

// Old-fashioned callback way
x = function(a, b, c, done) {
  if (success)
    done(null, "Result here");
  else
    done("uh oh, error");
};

Wrap that function in defer instead. (See defer())

// New defer.js way
x = defer(function(a, b, c, next) {
  if (success)
    next("Result here");
  else
    throw "uh oh, error";
});

When invoking another async function, wrap the callback in next.wrap too. This will catch errors inside that function: (See next.wrap())

x = defer(function(a, b, c, next) {
  $.get('/', next.wrap(function() { /* <-- here */
    if (success)
      next("Result here");
    else
      throw "uh oh, error";
  });
});

Bonus: now your function can be used as a promise or a regular callback-powered async:

// Callback style
// (called with a function as the last param)
x(a, b, c, function(err, result) {
  if (err)
    console.log("Fail:", err);
  else
    console.log("OK:", result);
});
// Promise/A+ style
// (called without a function in the last param)
x(a, b, c)
  .then(function(result) {
    console.log("OK:", result);
  }, function(err) {
    console.log("Fail:", err);
  });

What it solves

What follows is a long-winding explanation of Defer.js reason for living. If you're already convinced of its need for existence, skip on over to API.

Error catching

Perhaps the most inelegant thing about asynchronous JavaScript callbacks is error handling. Or rather: proper error handling.

To illustrate how this can get particularly hairy, let's start with an innocent function that expects a Node-style callback:

/**
 * Fetch the feed user (via AJAX) for a given `user`.
 */
getFeed = function(user, done) {
  var id = user.name.toLowerCase();

  $.get('/user/'+id+'/feeds.json', function(data) {
    if (data.entries)
      done(null, data);
    else
      done("No such user");
  });
};

This function expects an argument (done) callback that can be passed errors or data. Great! It can return errors! This is the style that most of the Node.js API is written in (along with thousands of Node packages), so it's got to be a good idea. Let's try to put it to test:

var john = {
  email: "john@gmail.com",
  name: "John"
};

getFeed(john, function(err, data) {
  if (err) console.log("Error:", err);
  console.log("John's entries:", data);
});

We just wrote a function that captures an errors (if (err) ...), or consumes the data otherwise. That's got to work right! Until it does something unfortunately unexpected:

var john = {
  email: "john@gmail.com",
  name: null  /* <-- uh oh. why doesn't he have a name? */
}; 

getFeed(john, function(err, data) {
  if (err) console.log("Error:", err);
  console.log("John's entries:", data);
});
TypeError: Cannot call method 'toLowerCase' of null
  at feed.js:5 [var id = user.name.toLowerCase();]

Gasp! Shouldn't this error have been caught and handled? Of course not--we never put any provisions to catch it. No problem, we can rewrite that getFeed() function to put its contents in a try/catch block.

getFeed = function(user, done) {
  try {
    var id = user.name.toLowerCase();

    $.get('/user/'+id+'/feeds.json', function(data) {
      if (data.entries)
        done(null, data);
      else
        done("No such user");
    });
  }
  catch (err) { /* <-- alright, let's relay some errors to the callback. */
    done(err);
  }
});

This works as expected, but wrapping all your functions in a try/catch blocks cat be a very cathartic exercise. Defer.js to the rescue! Simply wrap your function inside defer(...) and it'll take care of that for you.

Instead of writing x = function(a,b,c,done) { ... }, use x = defer(function(a,b,c,next) { ... });. Notice how errors are now simply thrown instead of being passed manually.

var defer = require('defer');

// Wrap your function inside `defer(...)`.
getFeed = defer(function(user, next) {
  var id = user.name.toLowerCase();

  $.get('/user/'+id+'/feeds.json', function(data) {
    if (data.entries)
      next(data);
    else
      throw "No such user";
  });
});

Now you got your errors trapped and passed for you. Let's try to consume getFeed() again:

var john = null;
getFeed(john, function(err, data) {
  if (err) {
    console.log("Uh oh! Caught an error.");
    console.log("=> "+ err);
    return;
  }
  console.log("John's entries:", data);
});

This now catches the error in err as we expected.

Uh oh! Caught an error.
=> TypeError: Cannot call method 'toLowerCase' of null

Deep error catching

"So what? We can easily write this decorator without defer.js," you may be thinking. In fact, it's this very line of thinking that got me to writing defer.js in the first place.

Let's move on to a more complex example. Let's say we're writing an async function to fetch some data, crunch it, and return it.

/*
 * Fetches posts and gets the title of the first post.
 */
getFirstPost = function(next) {
  $.get('/posts.json', function(data) {
    var post = data.entries[0].title;
    next(null, post);
  });
};

Let's use it:

getFirstPost(function(title) {
  $("h1").html(title);
});

It works, but it'll get you an unexpected result in some circumstances. What if data.entries is empty?

TypeError: Cannot read property 'title' of undefined
  at getfirstpost.js:6 [data.entries[0].title]

Uh oh: we have an error that happens in an async callback. We need to catch that too. Without defer.js, we may need to do 2 try/catch blocks: one for inside the function body, and another for inside the callback function's body. This is borderline asinine.

getFirstPost = function(next) {
  try {
    $.get('/posts.json', function(data) {
      try {
        var post = data.entries[0].title;
        next(null, post);
      }
      catch (err) {
        next(err);
      }
    });
  } catch (err) {
    next(err);
  }
}

Defer.js provides a next.wrap() function that wraps any new callback for you, which ensures that any errors it throws gets propagated properly. That colossal function can be written more concisely with Defer.js:

getFirstPost = defer(function(next) {
  $.get('/posts.json', next.wrap(function(data) {
    var post = data.entries[0].title;
    next(post);
  }));
});

Working with promises

Get Promise support by tying it in with your favorite Promise library. You can swap it out by changing defer.promise to the provider of when.js, q.js, promise.js or anything else that follows their API.

var defer = require('defer');

defer.promise = require('q').promise;
defer.promise = require('when').promise;
defer.promise = require('promise');

Call it with promises or not

Just write any defer.js-powered async function and it can work with Node-style callbacks or promises. The same getFirstPost() function we wrote can be used as a promise:

// As promises
getFirstPost()
  .then(function(title) {
    $("h1").html(title);
  });

or it can be invoked with a callback:

// As a Node-style async function
getFirstPost(function(err, title) {
  $("h1").html(title);
});

Use it to run promises

In the real world, you may be using libraries that only support Promises, and have it play safe with libraries that use traditional callbacks.

Defer.js helps you with this. Any defer-powered function you write can use promises. Instead of using the next() callback, make it return a promise object: defer automatically knows what to do.

getFirstPost = defer(function() {
  return $.get("/posts.json")
  .then(function(data) {
    return data.entries[0];
  })
  .then(function(post) {
    return post.title;
  });
});

You now get a function that can be used as a promise or an async function.

getFirstPost(function(err, data) {
  // used with a callback
});

getFirstPost()
.then(function(data) {
  // used as a promise
});

API

defer(fn)

A decorator that creates a function derived from fn, enhanced with defer.js superpowers.

When this new function is invoked (getName in the example below), it runs fn with the same arguments ([a] below), except with the last callback replaced with a new callback called next().

When next() is invoked inside [a], the callback given ([b]) will run. (next() is described in detail later below.)

getName = defer(function(next) { //[a]
  next("John");
});

getName(function(err, name) { //[b]
  alert("Hey " + name);
});

All arguments will be passed through. In the example below, the names passed onto man and companion are passed through as usual, but the last argument (a function) has been changed to next.

getMessage = defer(function(man, companion, next) {
  var msg = "How's it goin, " + man + " & " + companion);
  next(msg);
});

getMessage("Doctor", "Donna", function(err, msg) {
  alert(msg);
  /* => "How's it goin, Doctor & Donna" */
});

Any errors thrown inside fn will be passed the callback.

getName = defer(function(next) {
  var name = user.toUpperCase();
  next("John");
});

getName(function(err, data) {
  if (err) {
    /* err.message === "Cannot call method 'toUpperCase' of undefined" */
  }
});

next()

Returns an error, or a result, to the callback.

You can return a result by calling next(result).

getName = defer(function(next) {
  next("John");
});

getName(function(err, name) {
  alert("Hey " + name);
});

Returning errors

You may also return errors. You can do this by throwing.

getName = defer(function(next) {
  throw new Error("Something happened");
}

getName(function(err, name) {
  if (err) {
    alert(err.message); //=> "Something happened"
  }
});

Wrapping other callbacks

When next() is invoked with a function as an argument, it wraps ("decorates") that function to ensure that any errors it produces is propagated properly. See next.wrap().

getArticles = defer(function(next) {
  $.get('/articles.json', next(function(data) {
    var articles = data.articles;
    next(articles);
  }));
};

getArticles(function(err, articles) {
  if (err)
    console.error("Error:", err);
    /*=> "TypeError: cannot read property 'articles' of undefined" */
  else
    console.log("Articles:", articles);
});

With promises

You can also return a from the function. Defer will automatically figure out what to do from that.

getFirstPost = defer(function() {
  return $.get("/posts.json")
  .then(function(data) {
    return data.entries[0];
  })
  .then(function(post) {
    return post.title;
  });
});

You now get a function that can be used as a promise or an async function.

getFirstPost(function(err, data) {
  // used with a callback
});

getFirstPost()
.then(function(data) {
  // used as a promise
});

next.ok()

Returns a result. This is the same as calling next().

getName = defer(function(next) {
  if (user.name)
    next(user.name);
  else
    throw "User has no name";
}

next.err()

Returns an error. This is the same as throwing an error, but is convenient when used inside deeper callbacks that you can't wrap with next.wrap.

getName = defer(function(next) {
  $.get("/user.json")
  .then(function(data) {
    if (!data.name)
      next.err("oops, no name here");
  })
}

next.wrap()

Wraps a function ("decorates") to ensure that all errors it throws are propagated properly.

When next() is invoked with a function as an argument, it works the same way as next.wrap().

In this example below, any errors happening within the function [a] will be reported properly.

getArticles = defer(function(next) {
  $.get('/articles.json', next.wrap(function(data) { //[a]
    var articles = data.articles;
    next(articles);
  }));
};

getArticles(function(err, articles) {
  if (err)
    console.error("Error:", err);
    /*=> "TypeError: cannot read property 'articles' of undefined" */
  else
    console.log("Articles:", articles);
});

defer.promise

The provider function.

(function(factory) {
if (typeof module !== 'undefined') module.exports = factory();
else this.defer = factory();
})(function() {
var immediate;
/**
* Promise/async shim.
*/
var defer = function(fn) {
// Return a function that decorates the original `fn`.
return function() {
var self = this;
var last = arguments[arguments.length-1];
var args = [].slice.call(arguments, 0, arguments.length-1);
// Create the `next` handler.
var next = _next();
next.wrap = _wrap(next);
// Create the invoker function that, when called, will run `fn()` as needed.
var invoke = _invoke(fn, args, next, self);
// Used as an async:
// The function was invoked with a callback at the `last` argument.
if (typeof last === 'function') {
var callback = last;
next.err = function(err) {
callback.call(self, err); };
next.ok = function(result) {
callback.apply(self, [undefined].concat([].slice.call(arguments))); };
next.progress = function() {};
return invoke();
}
// Used as a promise:
// The function was invoked without a callback; ensure that it returns a promise.
else {
if (!defer.promise)
throw new Error("No promises support (defer.promise not defined)");
var promise = new defer.promise(function(ok, err, progress) {
next.ok = ok;
next.err = err;
next.progress = progress;
});
immediate(invoke);
return promise;
}
};
};
/**
* Creates a `wrap` decorator function.
*
* This creates a function `wrap` that taken an argument `fn`, executes it, and
* passes the errors to `next.err`.
*/
function _wrap(next) {
return function(fn) {
return function() {
try { fn.apply(this, arguments); }
catch (e) { next.err.call(this, e); }
};
};
}
/**
* Creates a `next` callback and returns it.
*
* This callback will delegate to `next.wrap()`, `next.ok()`, and `next.err()`
* depending on the arguments it was called with.
*
* - When called with an `Error` instance, it reports it to `next.err(...)`.
*
* - When called with a function, it runs it through `next.wrap(...)`.
*
* - Everything else is ran through `next.ok(...)`.
*/
function _next() {
return function next(result) {
next.ok.apply(this, arguments);
};
}
/**
* Creates an invoker function. This function will run `fn` (with arguments
* `args` and `next`, in context `self`), then report any errors caught to
* `next.err`.
*
* If the function `fn` returns a promise, it'll be passed to `next` as needed.
*
* This function will invoke the given `fn`, swapping out the last arg for
* a the callback `next`.
*/
function _invoke(fn, args, next, self) {
return function invoke() {
try {
var result = fn.apply(self, args.concat([next]));
if (result && result.then)
result.then(
next.ok,
function(err) { return next.err.call(this, err); },
next.progress);
return result;
} catch (err) {
next.err.call(this, err);
}
};
}
/**
* This is the promise provider.
*/
defer.promise = null;
/**
* Helper: shim for setImmediate().
*/
immediate =
(typeof setImmediate === 'function') ? setImmediate :
(typeof process === 'object') ? process.nextTick :
function(fn) { return setTimeout(fn, 0); };
return defer;
});
@noodlehaus
Copy link

this is great. i'm not so sure about the automatic promises tie-in though (Q in this case). there are a lot of promises implementations and a lot of them are already going for A+ compliance. maybe make the promises provider injectable? in my case, i always use RSVP.js.

@rstacruz
Copy link
Author

Cool—just updated it so that Q isn't a hard dependency. In fact, I threw in some unit tests for promise/when/rsvp (but not visible here).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment