Comparing version
var util = require('util') | ||
, domain = require('domain') | ||
, FormatStackTrace = require('./formatStackTrace') | ||
, Block = require('./control-block') | ||
, filename1 = __filename | ||
, filename2 = require.resolve('./hook') | ||
, hook = require('./hook') | ||
, hooked = false | ||
, fileNameFilter = [__filename, require.resolve('./hook'), 'domain.js'] | ||
, options = { | ||
'long-stack-traces': false | ||
'long-stack-traces': null | ||
} | ||
@@ -16,27 +17,135 @@ | ||
function trycatch(tryFn, catchFn) { | ||
if ('function' !== typeof tryFn || 'function' !== typeof catchFn) { | ||
throw new Error('tryFn and catchFn must be functions') | ||
} | ||
trycatch.begin(tryFn, catchFn) | ||
} | ||
trycatch.begin = Block.begin | ||
trycatch.guard = Block.guard | ||
trycatch.begin = domainTrycatch | ||
// Won't be used until long-stack-traces turned off=>on=>off | ||
trycatch.guard = nopGuard | ||
trycatch.configure = configure | ||
function configure(opts) { | ||
util._extend(options, opts) | ||
if ('undefined' !== typeof opts['long-stack-traces']) { | ||
options['long-stack-traces'] = Boolean(opts['long-stack-traces']) | ||
} | ||
if (!!opts['long-stack-traces']) { | ||
if (true === options['long-stack-traces']) { | ||
if (!hooked) { | ||
hooked = true | ||
// Replace built-in async functions, shim callbacks with generator | ||
hookit() | ||
} | ||
// findToken fails when _TOKEN_ deeper than Error.stackTraceLimit | ||
Error.stackTraceLimit = Infinity | ||
trycatch.guard = longStackTracesGuard | ||
trycatch.begin = begin | ||
} else if (typeof opts['long-stack-traces'] !== 'undefined') { | ||
trycatch.begin = longStackTraceBegin | ||
trycatch.guard = longStackTraceGuard | ||
} else if (false === options['long-stack-traces'] && hooked) { | ||
console.warn('Turning long-stack-traces off can result in indeterminate behavior.') | ||
Error.stackTraceLimit = 10 | ||
trycatch.begin = Block.begin | ||
trycatch.guard = Block.guard | ||
trycatch.begin = domainTrycatch | ||
trycatch.guard = nopGuard | ||
} | ||
} | ||
function begin(tryFn, catchFn) { | ||
function hookit() { | ||
hook(function generateShim(callback, fnName) { | ||
return trycatch.guard(callback, fnName) | ||
}) | ||
} | ||
// In case long-stack-traces are switched on -> off | ||
function nopGuard(cb) { | ||
return cb | ||
} | ||
function domainTrycatch (fn, cb, token) { | ||
var parentDomain = domain.active | ||
, d = domain.create() | ||
d.on('error', function onError(err) { | ||
var isError = err instanceof Error | ||
if (true === options['long-stack-traces'] && '_TOKEN_' === fn.name) { | ||
if (isError) { | ||
err = handleError(err) | ||
} else { | ||
console.warn('Unable to generate long-stack-trace for thrown non-Error') | ||
} | ||
} | ||
if (!isError) { | ||
err = new Error(err) | ||
} | ||
runInDomain(parentDomain, function() { | ||
cb(err) | ||
}) | ||
}) | ||
runInDomain(d, fn) | ||
} | ||
function runInDomain(d, fn) { | ||
if (d && !d._disposed) { | ||
try { | ||
d.run(fn) | ||
} catch(e) { | ||
d.emit('error', e) | ||
} | ||
} else { | ||
fn() | ||
} | ||
} | ||
function handleError(err) { | ||
var parent | ||
, token | ||
, skip = false | ||
if (err.token) { | ||
skip = true | ||
token = err.token | ||
} else { | ||
parent = generateStack(err, stackSearch).stack | ||
err.stack = parent.stack | ||
if (!parent.token) { | ||
// options['long-stack-traces'] was probably toggled after async was invoked | ||
return err | ||
} | ||
token = parent.token | ||
parent = null | ||
} | ||
while(token.error) { | ||
if (!skip && token.catchFn) break | ||
skip = false | ||
// HACK: Use Error.prepareStackTrace = stackSearch to find parent | ||
parent = generateStack(token.error, stackSearch).stack | ||
if (!parent) { | ||
throw err | ||
} | ||
if (!token.catchFn && parent.stack) { | ||
err.stack += '\n ----------------------------------------\n' + | ||
' at '+token.parent+'\n' + | ||
parent.stack.substring(parent.stack.indexOf("\n") + 1) | ||
} | ||
token = parent.token | ||
} | ||
if ('function' === typeof token.catchFn) { | ||
err.token = token | ||
return err | ||
} | ||
} | ||
function longStackTraceBegin(tryFn, catchFn) { | ||
// Create origin _TOKEN_ for stack termination | ||
function _TOKEN_() { | ||
tryFn() | ||
tryFn() | ||
} | ||
@@ -47,15 +156,11 @@ _TOKEN_.catchFn = catchFn | ||
try { | ||
_TOKEN_() | ||
} catch (err) { | ||
catchFn(generateStack(err)) | ||
} | ||
domainTrycatch(_TOKEN_, catchFn) | ||
} | ||
function generateStack(err, fn) { | ||
if (typeof fn !== 'function') { | ||
if ('function' !== typeof fn) { | ||
fn = function(error, structuredStackTrace) { | ||
return FormatStackTrace(error | ||
, structuredStackTrace | ||
, [filename1, filename2] | ||
, fileNameFilter | ||
, trycatch.colors) | ||
@@ -67,2 +172,3 @@ } | ||
Error.prepareStackTrace = fn | ||
// Generate stack trace by accessing stack property | ||
err.stack = err.stack | ||
@@ -74,12 +180,12 @@ Error.prepareStackTrace = old | ||
// Generate a new callback wrapped in _TOKEN_ with Error to trace back | ||
function longStackTracesGuard(next, name, location) { | ||
if (typeof next !== 'function') return next | ||
function longStackTraceGuard(callback, name) { | ||
var parentDomain | ||
// _TOKEN_ is the new callback and calls the real callback, next() | ||
if ('function' !== typeof callback) return callback | ||
parentDomain = domain.active | ||
// _TOKEN_ is the new callback and calls the real callback, callback() | ||
function _TOKEN_() { | ||
try { | ||
return next.apply(this, arguments) | ||
} catch (e) { | ||
handleError(e, _TOKEN_, false) | ||
} | ||
callback.apply(this, arguments) | ||
} | ||
@@ -90,60 +196,27 @@ | ||
return _TOKEN_ | ||
} | ||
function handleError(err, token, recursive) { | ||
var parent | ||
if (!recursive) { | ||
if (!err.token) { | ||
// Newly created Error | ||
err = err instanceof Error ? err : new Error(''+err) | ||
err = generateStack(err) | ||
err.parentalStack = err.stack | ||
} else { | ||
token = err.token | ||
} | ||
return function() { | ||
var args = arguments | ||
runInDomain(parentDomain, function() { | ||
_TOKEN_.apply(this, args) | ||
}) | ||
} | ||
while(token.error) { | ||
// HACK: Use Error.prepareStackTrace = stackSearch to find parent | ||
parent = generateStack(token.error, stackSearch).stack | ||
if (!parent) throw err | ||
if (!token.catchFn && parent.stack) { | ||
err.stack += '\n ----------------------------------------\n' + | ||
' at '+token.parent+'\n' + | ||
parent.stack.substring(parent.stack.indexOf("\n") + 1) | ||
} | ||
token = parent.token | ||
if (token.catchFn) break | ||
} | ||
if (typeof token.catchFn === 'function') { | ||
err.token = token | ||
try { | ||
token.catchFn.call(null, err, token) | ||
} catch(e2) { | ||
handleError(e2, token, true) | ||
} | ||
} | ||
} | ||
function stackSearch(error, structuredStackTrace) { | ||
var stack | ||
if (!structuredStackTrace) return | ||
stack = FormatStackTrace(error, structuredStackTrace, fileNameFilter, trycatch.colors) | ||
for (var fn, i=0, l=structuredStackTrace.length; i<l; i++) { | ||
fn = structuredStackTrace[i].fun | ||
if (fn.name === '_TOKEN_') { | ||
if ('_TOKEN_' === fn.name) { | ||
return { | ||
token: fn, | ||
stack: FormatStackTrace(error, structuredStackTrace, [filename1, filename2], trycatch.colors) | ||
stack: stack | ||
} | ||
} | ||
} | ||
} | ||
// Replace built-in async functions, shim callbacks with generator | ||
require('./hook')(function generateShim(next, name, location) { | ||
return trycatch.guard(next, name, location) | ||
}) | ||
return {stack: stack} | ||
} |
{ | ||
"name": "trycatch", | ||
"version": "0.1.5", | ||
"description": "An asynchronous exception handler with long stack traces for node.js", | ||
"version": "0.2.0", | ||
"description": "An asynchronous domain-based exception handler with long stack traces for node.js", | ||
"homepage": "http://github.com/CrabDude/trycatch", | ||
@@ -19,4 +19,5 @@ "repository": { | ||
"engines": { | ||
"node": ">=0.2.0" | ||
"node": ">=0.9.5" | ||
}, | ||
"engineStrict": true, | ||
"scripts": { | ||
@@ -23,0 +24,0 @@ "test": "mocha --reporter spec ./test/*.js" |
@@ -15,16 +15,29 @@ var trycatch = require('../lib/trycatch') | ||
trycatch.configure({ | ||
'long-stack-traces': !!longStackTraces | ||
'long-stack-traces': Boolean(longStackTraces) | ||
}) | ||
}) | ||
it('should require tryFn to be a function', function() { | ||
assert.throws(function() { | ||
trycatch(null, function() {}) | ||
}, Error) | ||
}) | ||
it('should require catchFn to be a function', function() { | ||
assert.throws(function() { | ||
trycatch(function() {}, null) | ||
}, Error) | ||
}) | ||
it('should catch Error object thrown synchronously', function(done) { | ||
trycatch(function() { | ||
(function foo() { | ||
throw new Error('Sync') | ||
})() | ||
}, function(err) { | ||
assert.equal(err.message, 'Sync') | ||
assert.notEqual(err.stack, undefined) | ||
done() | ||
}) | ||
(function foo() { | ||
throw new Error('Sync') | ||
})() | ||
} | ||
, function(err) { | ||
assert.equal(err.message, 'Sync') | ||
assert.notEqual(err.stack, undefined) | ||
done() | ||
}) | ||
}) | ||
@@ -34,10 +47,11 @@ | ||
trycatch(function() { | ||
process.nextTick(function() { | ||
throw new Error('Async') | ||
process.nextTick(function() { | ||
throw new Error('Async') | ||
}) | ||
} | ||
, function(err) { | ||
assert.equal(err.message, 'Async') | ||
assert.notEqual(err.stack, undefined) | ||
done() | ||
}) | ||
}, function(err) { | ||
assert.equal(err.message, 'Async') | ||
assert.notEqual(err.stack, undefined) | ||
done() | ||
}) | ||
}) | ||
@@ -48,2 +62,2 @@ }) | ||
run(false) | ||
run(true) | ||
run(true) |
@@ -19,3 +19,3 @@ var trycatch = require('../lib/trycatch') | ||
trycatch.configure({ | ||
'long-stack-traces': !!longStackTraces | ||
'long-stack-traces': Boolean(longStackTraces) | ||
}) | ||
@@ -101,2 +101,2 @@ }) | ||
run(false) | ||
run(true) | ||
run(true) |
@@ -16,3 +16,3 @@ var assert = require('assert'), | ||
trycatch.configure({ | ||
'long-stack-traces': !!longStackTraces | ||
'long-stack-traces': Boolean(longStackTraces) | ||
}) | ||
@@ -28,19 +28,21 @@ }) | ||
trycatch(function () { | ||
++count | ||
throw new Error('test 1') | ||
} | ||
, function(err) { | ||
++count | ||
assert.equal(err.stack.split(delimitter).length, 1) | ||
throw err | ||
}) | ||
} | ||
, function(err) { | ||
++count | ||
throw new Error('test 1') | ||
}, function(err) { | ||
++count | ||
// assert.equal(err.stack.split(delimitter).length, 1) | ||
throw err; | ||
assert.notEqual(err.name, 'AssertionError') | ||
assert.equal(err.message, 'test 1') | ||
assert.equal(count, 3) | ||
if (longStackTraces) { | ||
assert.equal(err.stack.split(delimitter).length, 1) | ||
} | ||
done() | ||
}) | ||
}, function(err) { | ||
++count | ||
assert.notEqual(err.name, 'AssertionError') | ||
assert.equal(err.message, 'test 1') | ||
assert.equal(count, 3) | ||
if (longStackTraces) { | ||
assert.equal(err.stack.split(delimitter).length, 1) | ||
} | ||
done(); | ||
}) | ||
}) | ||
@@ -52,27 +54,29 @@ | ||
trycatch(function () { | ||
setTimeout(function() { | ||
trycatch(function () { | ||
setTimeout(function() { | ||
++count | ||
throw new Error('test 2') | ||
}, 0) | ||
}, function(err) { | ||
++count | ||
if (longStackTraces) { | ||
assert.equal(err.stack.split(delimitter).length, 2) | ||
} | ||
throw err; | ||
}) | ||
}, 0) | ||
}, function(err) { | ||
++count | ||
assert.notEqual(err.name, 'AssertionError') | ||
assert.equal(err.message, 'test 2') | ||
assert.equal(count, 3) | ||
if (longStackTraces) { | ||
assert.equal(err.stack.split(delimitter).length, 3) | ||
setTimeout(function() { | ||
trycatch(function () { | ||
setTimeout(function() { | ||
++count | ||
throw new Error('test 2') | ||
}, 0) | ||
} | ||
, function(err) { | ||
++count | ||
if (longStackTraces) { | ||
assert.equal(err.stack.split(delimitter).length, 2) | ||
} | ||
throw err | ||
}) | ||
}, 0) | ||
} | ||
done(); | ||
}) | ||
, function(err) { | ||
++count | ||
assert.notEqual(err.name, 'AssertionError') | ||
assert.equal(err.message, 'test 2') | ||
assert.equal(count, 3) | ||
if (longStackTraces) { | ||
assert.equal(err.stack.split(delimitter).length, 3) | ||
} | ||
done() | ||
}) | ||
}) | ||
@@ -84,30 +88,32 @@ | ||
trycatch(function () { | ||
setTimeout(function() { | ||
trycatch(function () { | ||
setTimeout(function() { | ||
++count | ||
throw new Error('test 3') | ||
}, 0) | ||
}, function(err) { | ||
setTimeout(function() { | ||
++count | ||
if (longStackTraces) { | ||
assert.equal(err.stack.split(delimitter).length, 2) | ||
setTimeout(function() { | ||
trycatch(function () { | ||
setTimeout(function() { | ||
++count | ||
throw new Error('test 3') | ||
}, 0) | ||
} | ||
throw err; | ||
}, 0) | ||
}) | ||
}, 0) | ||
}, function(err) { | ||
++count | ||
assert.notEqual(err.name, 'AssertionError') | ||
assert.equal(err.message, 'test 3') | ||
assert.equal(count, 3) | ||
if (longStackTraces) { | ||
assert.equal(err.stack.split(delimitter).length, 3) | ||
, function(err) { | ||
setTimeout(function() { | ||
++count | ||
if (longStackTraces) { | ||
assert.equal(err.stack.split(delimitter).length, 2) | ||
} | ||
throw err | ||
}, 0) | ||
}) | ||
}, 0) | ||
} | ||
done(); | ||
}) | ||
, function(err) { | ||
++count | ||
assert.notEqual(err.name, 'AssertionError') | ||
assert.equal(err.message, 'test 3') | ||
assert.equal(count, 3) | ||
if (longStackTraces) { | ||
assert.equal(err.stack.split(delimitter).length, 3) | ||
} | ||
done() | ||
}) | ||
}) | ||
@@ -118,2 +124,2 @@ }) | ||
run(false) | ||
run(true) | ||
run(true) |
@@ -20,3 +20,3 @@ var assert = require('assert'), | ||
trycatch.configure({ | ||
'long-stack-traces': !!longStackTraces | ||
'long-stack-traces': Boolean(longStackTraces) | ||
}) | ||
@@ -27,10 +27,11 @@ }) | ||
trycatch(function () { | ||
setTimeout(function () { | ||
throw 'my-string being thrown' | ||
}, 0) | ||
}, function onError(err) { | ||
assert.equal(err.message, 'my-string being thrown') | ||
assert.notEqual(err.stack, undefined) | ||
done() | ||
}) | ||
setTimeout(function () { | ||
throw 'my-string being thrown' | ||
}, 0) | ||
} | ||
, function onError(err) { | ||
assert.equal(err.message, 'my-string being thrown') | ||
assert.notEqual(err.stack, undefined) | ||
done() | ||
}) | ||
}) | ||
@@ -40,10 +41,11 @@ | ||
trycatch(function () { | ||
setTimeout(function () { | ||
throw 123 | ||
}, 0) | ||
}, function onError(err) { | ||
assert.equal(err.message, (123).toString()) | ||
assert.notEqual(err.stack, undefined) | ||
done() | ||
}) | ||
setTimeout(function () { | ||
throw 123 | ||
}, 0) | ||
} | ||
, function onError(err) { | ||
assert.equal(err.message, String(123)) | ||
assert.notEqual(err.stack, undefined) | ||
done() | ||
}) | ||
}) | ||
@@ -55,10 +57,11 @@ | ||
trycatch(function () { | ||
setTimeout(function () { | ||
throw true | ||
}, 0) | ||
}, function onError(err) { | ||
assert.equal(err.message, (true).toString()) | ||
assert.notEqual(err.stack, undefined) | ||
done() | ||
}) | ||
setTimeout(function () { | ||
throw true | ||
}, 0) | ||
} | ||
, function onError(err) { | ||
assert.equal(err.message, String(true)) | ||
assert.notEqual(err.stack, undefined) | ||
done() | ||
}) | ||
}) | ||
@@ -69,2 +72,3 @@ }) | ||
run(false) | ||
run(true) | ||
// Unable to generate long-stack-traces: Throwing non-Errors is incompatible with domains | ||
run(true) |
48526
-2.14%15
-6.25%772
-4.34%