0

I’m introducing promises using the Bluebird library to my Javascript code, as they seem to be the closest available thing to the flow control combinators I’m used to in Haskell. The following Gulp task works for me:

gulp.task('upload', function(callback) {
  configAWSFromFile();
  var functionName = 'redacted',
      Lambda = new AWS.Lambda();
  Promise.promisifyAll(Object.getPrototypeOf(Lambda));

  function uploadLambdaFn(lambdaFn, fileStream) {
    var params = {
      FunctionName: functionName,
      Handler: lambdaFn.Configuration.Handler,
      // snip
      FunctionZip: fileStream
    };
    return Lambda.uploadFunctionAsync(params);
  }

  return Promise.join(Lambda.getFunctionAsync({FunctionName: functionName}),
                      fs.readFileAsync(path.join(destPrefix, uploadPackage)),
                      uploadLambdaFn)
    .then(function(response) {
      gutil.log('Response:\n' + util.inspect(response));
    });
});

However, when I try to refactor the upload step out of the config object creation function, it breaks down:

gulp.task('upload', function(callback) {
  configAWSFromFile();
  var functionName = 'redacted',
      Lambda = new AWS.Lambda();
  Promise.promisifyAll(Object.getPrototypeOf(Lambda));

  // rename function
  function mkLambdaFnConfig(lambdaFn, fileStream) {
    var params = {
      FunctionName: functionName,
      Handler: lambdaFn.Configuration.Handler,
      // snip
      FunctionZip: fileStream
    };
    return params;  // return immediate value
  }

  return Promise.join(Lambda.getFunctionAsync({FunctionName: functionName}),
                      fs.readFileAsync(path.join(destPrefix, uploadPackage)),
                      Promise.method(mkLambdaFnConfig))  // wrap regular function
    .then(Lambda.uploadFunctionAsync)  // add the upload to the chain
    .then(function(response) {
      gutil.log('Response:\n' + util.inspect(response));
    });
});

Gives the following stack trace (paths have been edited to be relative):

TypeError: undefined is not a function
    at svc.(anonymous function) (./node_modules/aws-sdk/lib/service.js:399:21)
    at tryCatcher (./node_modules/bluebird/js/main/util.js:24:31)
    at ret (eval at <anonymous> (./node_modules/bluebird/js/main/promisify.js:154:12), <anonymous>:13:39)
    at tryCatcher (./node_modules/bluebird/js/main/util.js:24:31)
    at Promise._settlePromiseFromHandler (./node_modules/bluebird/js/main/promise.js:466:31)
    at Promise._settlePromiseAt (./node_modules/bluebird/js/main/promise.js:545:18)
    at Promise._settlePromises (./node_modules/bluebird/js/main/promise.js:661:14)
    at Async._drainQueue (./node_modules/bluebird/js/main/async.js:79:16)
    at Async._drainQueues (./node_modules/bluebird/js/main/async.js:89:10)
    at Immediate.Async.drainQueues [as _onImmediate] (./node_modules/bluebird/js/main/async.js:14:14)
    at processImmediate [as _immediateCallback] (timers.js:358:17)

I get an identical stack trace if I change the mkLambdaFnConfig function to return Promise.resolve(params) instead of wrapping mkLambdaFnConfig in Promise.method:

gulp.task('upload', function(callback) {
  configAWSFromFile();
  var functionName = 'redacted',
      Lambda = new AWS.Lambda();
  Promise.promisifyAll(Object.getPrototypeOf(Lambda));

  // rename function
  function mkLambdaFnConfig(lambdaFn, fileStream) {
    var params = {
      FunctionName: functionName,
      Handler: lambdaFn.Configuration.Handler,
      // snip
      FunctionZip: fileStream
    };
    return Promise.resolve(params);  // return already-fulfilled promise
  }

  return Promise.join(Lambda.getFunctionAsync({FunctionName: functionName}),
                      fs.readFileAsync(path.join(destPrefix, uploadPackage)),
                      mkLambdaFnConfig)  // no additional wrapping
    .then(Lambda.uploadFunctionAsync)  // add the upload to the chain
    .then(function(response) {
      gutil.log('Response:\n' + util.inspect(response));
    });
});

Generates the stack trace (again, with relative paths):

TypeError: undefined is not a function
    at svc.(anonymous function) (./node_modules/aws-sdk/lib/service.js:399:21)
    at tryCatcher (./node_modules/bluebird/js/main/util.js:24:31)
    at ret (eval at <anonymous> (./node_modules/bluebird/js/main/promisify.js:154:12), <anonymous>:13:39)
    at tryCatcher (./node_modules/bluebird/js/main/util.js:24:31)
    at Promise._settlePromiseFromHandler (./node_modules/bluebird/js/main/promise.js:466:31)
    at Promise._settlePromiseAt (./node_modules/bluebird/js/main/promise.js:545:18)
    at Promise._settlePromises (./node_modules/bluebird/js/main/promise.js:661:14)
    at Async._drainQueue (./node_modules/bluebird/js/main/async.js:79:16)
    at Async._drainQueues (./node_modules/bluebird/js/main/async.js:89:10)
    at Immediate.Async.drainQueues [as _onImmediate] (./node_modules/bluebird/js/main/async.js:14:14)
    at processImmediate [as _immediateCallback] (timers.js:358:17)

I suspect that I’m missing some basic component of how promises work. I had thought that Promise.resolve and Promise.method were lifting functions like monadic return and applicative pure, but I’m guessing now that’s a gross misunderstanding.

Edit for future visitors:

This is the working code I ended up with (after @Bergi pointed out that it’s just a binding issue):

gulp.task('upload', function(callback) {
  configAWSFromFile();
  var functionName = 'redacted',
      Lambda = new AWS.Lambda();
  // As noted by @Esailija, this should be moved to init.
  Promise.promisifyAll(Object.getPrototypeOf(Lambda));

  function mkLambdaFnConfig(lambdaFn, fileStream) {
    var params = {
      FunctionName: functionName,
      Handler: lambdaFn.Configuration.Handler,
      // snip
      FunctionZip: fileStream
    };
    return params;
  }

  return Promise.join(Lambda.getFunctionAsync({FunctionName: functionName}),
                      fs.readFileAsync(path.join(destPrefix, uploadPackage)),
                      Promise.method(mkLambdaFnConfig))  // wrap synchronous function
    // Methods added as callbacks need to be bound, just like usual!
    .then(Lambda.uploadFunctionAsync.bind(Lambda))
    .then(function(response) {
      gutil.log('Response:\n' + util.inspect(response));
    });
});
Jason Whittle
  • 751
  • 4
  • 23
  • I don't think that problem is with your understanding of promises. (Though you may want to notice that JavaScript is extremely lax about types, promises can't be nested, and it doesn't care whether you return a value or a promise - you can't really forget a `return` like in Haskell). – Bergi Feb 11 '15 at 22:41
  • Just of out of curiosity, which async combinators do you use in Haskell? – Bergi Feb 11 '15 at 22:45
  • 1
    Please move `Promise.promisifyAll(Object.getPrototypeOf(Lambda));` out of runtime code to your initialization code, you really don't want to call that function after your application has already been initialized :D – Esailija Feb 12 '15 at 10:37
  • 1
    Thank you, @Bergi, I got caught up in trying to understand the metaprogramming involved in promisification and missed that this was a simple binding issue. – Jason Whittle Feb 12 '15 at 19:05
  • @Bergi: For the primary abstraction of any given application, I frequently find it useful to design things in terms of infinite streams. Then I can use Michael Snoyman’s excellent [conduits](https://www.fpcomplete.com/user/snoyberg/library-documentation/conduit-overview). But most of the time in Haskell I’m using whatever library with the combinators provided by the same old typeclasses: Functor, Applicative, Monad, Arrow. – Jason Whittle Feb 12 '15 at 19:40

1 Answers1

0

The problem here is that you've changed the type of your return value; while your uploadLambdaFn function returns a Bluebird promise, your MkLambdaFn is not. In a statically-typed, strictly-typed program, the compiler would catch this for you. With Javascript, unless you know what to look for, it can be a tripup.

Your program is breaking because something's trying to call a method on your Promise object - but since it's not a promise, it doesn't have that method, and thus errors.

You can fix this by wrapping your return value in a promise. I don't remember how Bluebird implements this (and I can't access Github from my work), but I'll edit the answer later to provide some code. Promise.resolve() isn't working for you, but there should be something that should.

Evan Davis
  • 35,493
  • 6
  • 50
  • 57