I'm using Sequelize in an application that uses normal node.js style callbacks in most of its code. Sequelize uses promises though, so a lot of code ends up looking something like this:
model.find({where: {...}}).then (res) ->
callback(null, res)
.catch (err) ->
callback(err)
That works well most of the time, but if there's a problem within the callback then the catch block will run and invoke the callback a second time, this time with the err argument instead of res.
This is particularly a problem in unit test where we're using expect.js
, which throws exceptions and expects the test runner to catch them, but instead they're getting garbled by sequelize catch blocks and making it difficult to track down the actual problem.
To solve this problem I'd like to invoke the callback outside of the scope of the current promise, and have the error handler only ever deal with errors directly related to sequelize. How do I do this?
Since sequelize uses Bluebird promises, you should be able to do this:
model.find({where: {...}).nodeify(callback);
It seems that there are a few different solutions to this.
First, to clarify the problem I was having with a more detailed example. Here you can see that the .catch
handler gets invoked when the error is thrown in the callback, which isn't what I wanted:
callback = (err, res) ->
console.log "Called with err: #{err} res: #{res}"
throw new Error()
models.User.find({where: {...}}).then (user) ->
callback(null, user)
.catch (err) ->
callback(err)
Here's the output, where you can see the callback getting invoked twice:
Called with err: null res: [object SequelizeInstance:User]
Called with err: Error res: undefined
Unhandled rejection Error
at callback (./scripts/nodeify_test.coffee:5:15)
at [object Object].<anonymous> (./scripts/nodeify_test.coffee:10:5)
at [object Object].tryCatcher (./node_modules/sequelize/node_modules/bluebird/js/main/util.js:24:31)
at Promise._settlePromiseFromHandler (./node_modules/sequelize/node_modules/bluebird/js/main/promise.js:454:31)
at Promise._settlePromiseAt (./node_modules/sequelize/node_modules/bluebird/js/main/promise.js:530:18)
at Promise._settlePromises (./node_modules/sequelize/node_modules/bluebird/js/main/promise.js:646:14)
at Async._drainQueue (./node_modules/sequelize/node_modules/bluebird/js/main/async.js:177:16)
at Async._drainQueues (./node_modules/sequelize/node_modules/bluebird/js/main/async.js:187:10)
at Async.drainQueues (./node_modules/sequelize/node_modules/bluebird/js/main/async.js:15:14)
at process._tickCallback (node.js:419:13)
Changing the order of .then .catch
Bergi pointed to When is .then(success, fail) considered an antipattern for promises? which gives the example of calling .catch before .then, and I confirmed that solves the problem:
callback = (err, res) ->
console.log "Called with err: #{err} res: #{res}"
throw new Error()
models.User.find({where: {...}}).catch (err) ->
callback(err)
.then (user) ->
callback(null, user)
Here's the output:
Called with err: null res: [object SequelizeInstance:User]
Unhandled rejection Error
at callback (./scripts/nodeify_test.coffee:5:15)
at [object Object].<anonymous> (./scripts/nodeify_test.coffee:10:5)
at [object Object].tryCatcher (./node_modules/sequelize/node_modules/bluebird/js/main/util.js:24:31)
at Promise._settlePromiseFromHandler (./node_modules/sequelize/node_modules/bluebird/js/main/promise.js:454:31)
at Promise._settlePromiseAt (./node_modules/sequelize/node_modules/bluebird/js/main/promise.js:530:18)
at Promise._settlePromises (./node_modules/sequelize/node_modules/bluebird/js/main/promise.js:646:14)
at Async._drainQueue (./node_modules/sequelize/node_modules/bluebird/js/main/async.js:177:16)
at Async._drainQueues (./node_modules/sequelize/node_modules/bluebird/js/main/async.js:187:10)
at Async.drainQueues (./node_modules/sequelize/node_modules/bluebird/js/main/async.js:15:14)
at process._tickCallback (node.js:419:13)
Using .nodeify
cassjj also pointed to nodeify, which effectively turns the promises into normal node code:
callback = (err, res) ->
console.log "Called with err: #{err} res: #{res}"
throw new Error()
models.User.find({where: {...}}).nodeify callback
Here the output matches the .catch .then
example:
Called with err: null res: [object SequelizeInstance:User]
./node_modules/sequelize/node_modules/bluebird/js/main/async.js:43
fn = function () { throw arg; };
^
Error
at [object Object].callback (./scripts/nodeify_test.coffee:5:15)
at [object Object].tryCatcher (./node_modules/sequelize/node_modules/bluebird/js/main/util.js:24:31)
at Promise.successAdapter [as _fulfillmentHandler0] (./node_modules/sequelize/node_modules/bluebird/js/main/nodeify.js:22:30)
at Promise._settlePromiseAt (./node_modules/sequelize/node_modules/bluebird/js/main/promise.js:528:21)
at Promise._settlePromises (./node_modules/sequelize/node_modules/bluebird/js/main/promise.js:646:14)
at Async._drainQueue (./node_modules/sequelize/node_modules/bluebird/js/main/async.js:177:16)
at Async._drainQueues (./node_modules/sequelize/node_modules/bluebird/js/main/async.js:187:10)
at Async.drainQueues (./node_modules/sequelize/node_modules/bluebird/js/main/async.js:15:14)
at process._tickCallback (node.js:419:13)
I also confirmed that I see the same result even if I add a catch handler:
models.User.find({where: {...}}).nodeify callback
.catch (err) ->
callback.call({}, err)
I'm going to go with .nodeify
.