Skip to content

Instantly share code, notes, and snippets.

@OliverJAsh
Last active August 29, 2015 14:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save OliverJAsh/0c755bd05219fb8011cf to your computer and use it in GitHub Desktop.
Save OliverJAsh/0c755bd05219fb8011cf to your computer and use it in GitHub Desktop.
wrap: Nodeify the result of a promise returning function.

wrap

Nodeify the result of a promise returning function.

When an API requires that you pass it a function with the signature (...args, cb), wrap is a small helper that allows you to instead pass a promise returning function. wrap will nodeify the result of the promise returning function (fulfilment or rejection) by passing its value to the original callback.

Example

One such API that asks for an asynchronous listener function (...args, cb) is express. When a HTTP request is made, express will call your listener function, if you have one registered for that route. The example below demonstrates how wrap can help you to use promises in such a situation.

// 1. The traditional callback (next)
app.get('/', (req, res, next) => {
    next(new Error('Example error'));
});

// 2. If you want to use promises, you must remember to manually nodeify. That
// it to say, "call the callback with the result of the promise (fulilment
// or rejection)."
app.get('/', (req, res, next) => {
    return Q.reject(new Error('Example error'))
        .nodeify(next);
});

// 3. wrap nodeifies the promise for you.
app.get('/', wrap((req, res) => {
    return Q.reject(new Error('Example error'));
}));

To run the express example:

npm install
node traceur-runner.js express-example.js
import wrap from './wrap';
import Q from 'q';
import express from 'express';
var app = express();
app.get('/', wrap((req, res) => {
return Q.reject(new Error('Example error'));
}));
// Error handler: arity must be 4
app.use((error, req, res, next) => {
console.error('Express error handler:', error.stack);
res
.type('text/plain')
.status(500)
.send(error.stack);
});
var server = app.listen(3000, () => {
var { host, port } = server.address();
console.log('Example app listening at http://%s:%s', host, port);
});
{
"name": "wrap",
"version": "0.0.0",
"description": "Nodeify the result of a promise returning function.",
"main": "wrap.js",
"author": "Oliver Joseph Ash <oliverjash@gmail.com>",
"dependencies": {
"lodash-node": "^2.4.1",
"q": "^1.1.1"
},
"devDependencies": {
"express": "^4.10.2",
"traceur": "0.0.74",
"traceur-source-maps": "^1.0.5"
}
}
'use strict';
var traceur = require('traceur');
require('traceur-source-maps').install(traceur);
var path = require('path');
traceur.require.makeDefault(function (modulePath) {
var isDependency = modulePath.indexOf('node_modules') !== -1;
return ! isDependency;
});
require(path.resolve(process.cwd(), process.argv[2]));
import Q from 'q';
import toArray from 'lodash-node/modern/collections/toArray';
export default function wrap(promiseReturningFn) {
// ES5 function so we can access the arguments of *this* function
return function () {
var callback = arguments[arguments.length - 1];
var argumentsWithoutCallback = toArray(arguments).slice(0, -1);
var promise = promiseReturningFn(...argumentsWithoutCallback);
// Cast to Q so we can use the nodeify helper
Q(promise).nodeify(callback);
};
}
@jasonkarns
Copy link

Rather than wrapping at the call site, I would think it would be cleaner to modify the app.get/post methods. I see little difference between manually nodeify-ing vs manually wrapping.

@OliverJAsh
Copy link
Author

@jasonkarns wrapping means you can author functions that return promises as normal, and if you ever need to pass them to an API such as express’ router, you can just wrap them to receive a nodeified version.

Monkey patching is difficult for some APIs, and error prone when upgrading the dependency.

@jasonkarns
Copy link

Agreed on both counts.

Though if you want a clear separation of your code from consuming apis, they wouldn't be anonymous functions. In which case, I could see the handler functions existing on some type of router object, which could then have wrap mapped over its member functions to return the node-friendly version of the handlers.

To the second point, I agree in general (though the approach needn't be via monkey patching). However, if the dependency were to change (in this case, express) such that its api no longer looked like app.get(path, cb) then one wouldn't be able to blindly upgrade regardless. If anything, wrapping the express api with an adapter object would make your code more stable by having a translation point should the express api change. It would be in the adapter object where things would be nodified and could also handle the shock of future api changes.

@OliverJAsh
Copy link
Author

Very fair points.

I've just had a go at writing an adapter for express.Router. It's proving quite difficult, although mostly because it requires that I understand exactly how express.Router works (so I can mimic its interface for the adapter). In many cases when you don't have enough understanding about how a third party API works, it's easiest to fix it at the call site (as I'm proposing above).

I'm not really convinced it's worth trying to write a fully working express.Router adapter, given that I don't know enough about the required interface. Here's what I started with anyway:

var expressRouter = express.Router();

var router = {
    get: (route, listenerFn) => expressRouter.get(route, wrap(listenerFn)),
    post: (route, listenerFn) => expressRouter.post(route, wrap(listenerFn)),
    param: (param, listenerFn) => expressRouter.param(param, listenerFn)
};

router.get(, );

@OliverJAsh
Copy link
Author

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