Last active
July 20, 2017 01:30
-
-
Save martinheidegger/4c2b7908005f65f9a53b25d5fec65f63 to your computer and use it in GitHub Desktop.
An approach to debug promises that have not yet finished when the process is exited.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
const DATE_MAX = (36 * 36 * 36 * 36 * 36) // Since we use base 36 this should give us 5 characters | |
const RANDOM_MAX = 100000 | |
function createId () { | |
// ID consistent of a time aspect as well as a random aspect to make sure two Promises | |
return `${(Date.now() % DATE_MAX).toString(36)}-${(Math.random() * RANDOM_MAX | 0).toString(36)}` | |
} | |
function maxLines (max, input) { | |
var lines = String(input).split('\n') | |
if (lines.length > max) { | |
return `${lines.slice(0, max - 1).join('\n')} | |
... ${lines.length - max - 1} more lines ...` | |
} | |
return lines.join('\n') | |
} | |
const RUNNING = {} | |
function intercept (result, parent) { | |
result.thenHandler = 0 | |
result._then = result.then | |
result.then = handler => { | |
result.thenHandler += 1 | |
return result._then(data => { | |
var next = handler(data) | |
if (next instanceof Promise) { | |
return wrap(() => next, null, parent) | |
} | |
return next | |
}) | |
} | |
result._catch = result.catch | |
result.catchHandler = 0 | |
result.catch = handler => { | |
result.catchHandler += 1 | |
return result._catch(err => { | |
var next = handler(err) | |
if (next instanceof Promise) { | |
return wrap(() => next, null, parent) | |
} | |
return next | |
}) | |
} | |
return result | |
} | |
function wrap (initPromise, options, parent) { | |
const id = createId() | |
const stack = (new Error().stack).split('\n').slice(2).map((line) => line.trim()) | |
var promise | |
try { | |
promise = initPromise(options, id) | |
if (!(promise instanceof Promise)) { | |
promise = Promise.resolve(promise) | |
} | |
} catch (e) { | |
promise = Promise.reject(e) | |
} | |
var result = promise | |
.catch(e => { | |
result.catchHandler = 0 | |
delete RUNNING[id] | |
console.log(`DEBUG: Rejection ${e.stack || e}`) | |
return Promise.reject(e) | |
}) | |
.then(res => { | |
result.thenHandler = 0 | |
delete RUNNING[id] | |
return res | |
}) | |
result.id = id | |
result.parent = parent | |
result.stack = stack | |
result.initPromise = initPromise | |
result.options = options | |
RUNNING[id] = result | |
return intercept(result, id) | |
} | |
process.on('exit', () => { | |
const ids = Object.keys(RUNNING) | |
if (ids.length > 0) { | |
if (ids.length === 1) { | |
console.log(`1 Promise is still unfullfilled!`) | |
} else { | |
console.log(`${ids.length} Promises are still unfullfilled!`) | |
} | |
ids.forEach((id) => { | |
var promise = RUNNING[id] | |
var ids = [id.toUpperCase()] | |
var parent = promise.parent | |
while (parent) { | |
ids.unshift(parent) | |
var running = RUNNING[parent] | |
parent = running && running.parent | |
} | |
var options = '' | |
if (promise.options) { | |
options = ` | |
OPTIONS | |
${JSON.stringify(promise.options, null, 2).split('\n').join('\n ')}` | |
} | |
console.log(`--- (${ids.join(' / ')}) --- | |
${promise.stack.slice(0, 3).join('\n ')} | |
CODE | |
${maxLines(3, promise.initPromise.toString()).split('\n').join('\n ')} | |
${options}`) | |
if (promise.thenHandler > 1 || promise.catchhandler > 1) { | |
console.log(` | |
WARNING: | |
WARNING: There has been more than one then/catch handler attached to ${id.toUpperCase()}. | |
WARNING: This is indicative of a recursive Promise dependency. | |
WARNING:`) | |
} | |
console.log('---') | |
}) | |
} | |
console.log() | |
}) | |
exports.wrap = wrap |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
;(new Promise(() => {})).then(() => console.log('a executed')) | |
;(Promise.resolve('x')).then(() => console.log('b executed')) | |
// | |
// This will output "b executed". "a executed" will not be triggered because the first promise will be never resolved! | |
// |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
var debug = require('./debug-promise') | |
debug.wrap(() => new Promise(() => {})).then(() => console.log('a executed')) | |
debug.wrap(() => Promise.resolve('x')).then(() => console.log('b executed')) | |
// | |
// This will output something like: | |
// | |
// b executed | |
// 1 Promise is still unfullfilled! | |
// --- (V347P-M6K) --- | |
// at Object.<anonymous> (/debug-test2.js:5:7) | |
// at Module._compile (module.js:571:32) | |
// at Object.Module._extensions..js (module.js:580:10) | |
// | |
// CODE | |
// () => new Promise(() => {}) | |
// | |
// --- | |
// |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
var all = { | |
a: ['b', 'c'], | |
b: ['d'], | |
c: ['a'], // <- recursive | |
d: [] | |
} | |
var cache = {} | |
function getDeep (key) { | |
var promise = cache[key] | |
if (promise) { | |
return promise | |
} | |
console.log(`Examining ${key}`) | |
promise = new Promise((resolve, reject) => { | |
setImmediate(() => { | |
Promise.all(all[key].map(getDeep)).then(resolve).catch(reject) | |
}) | |
}) | |
cache[key] = promise | |
return promise | |
} | |
getDeep('a').then(allDeps => { | |
console.log(`All dependencies of 'a': ${allDeps}`) | |
}).catch(e => console.log(e.stack || e)) | |
// | |
// This will output something like: | |
// | |
// Examining a | |
// Examining b | |
// Examining c | |
// Examining d | |
// | |
// Because at one point a waits on the result of a which means there | |
// is no background process for the promise left running: the nodejs app closes | |
// |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
const debug = require('./debug-promise') | |
var all = { | |
a: ['b', 'c'], | |
b: ['d'], | |
c: ['a'], // <- recursive | |
d: [] | |
} | |
var cache = {} | |
function getDeep (key) { | |
var promise = cache[key] | |
if (promise) { | |
return promise | |
} | |
console.log(`Examining ${key}`) | |
promise = debug.wrap(() => new Promise((resolve, reject) => { | |
setImmediate(() => { | |
Promise.all(all[key].map(getDeep)).then(resolve).catch(reject) | |
}) | |
})) | |
cache[key] = promise | |
return promise | |
} | |
getDeep('a').then(allDeps => { | |
console.log(`All dependencies of 'a': ${allDeps}`) | |
}).catch(e => console.log(e.stack || e)) | |
// | |
// This will output something like: | |
// | |
// Examining a | |
// Examining b | |
// Examining c | |
// Examining d | |
// 2 Promises are still unfullfilled! | |
// --- (VQCRD-POO) --- | |
// at getDeep (/debug-test4.js:20:19) | |
// at Object.<anonymous> (/debug-test4.js:29:1) | |
// at Module._compile (module.js:571:32) | |
// | |
// CODE | |
// () => new Promise((resolve, reject) => { | |
// setImmediate(() => { | |
// ... 1 more lines ... | |
// | |
// | |
// WARNING: | |
// WARNING: There has been more than one then/catch handler attached to VQCRD-POO. | |
// WARNING: This is indicative of a recursive Promise dependency. | |
// WARNING: | |
// --- | |
// --- (VQCRJ-UCQ) --- | |
// at getDeep (/debug-test-4.js:20:19) | |
// at Array.map (native) | |
// at Immediate.setImmediate (/debug-test-4.js:22:28) | |
// | |
// CODE | |
// () => new Promise((resolve, reject) => { | |
// setImmediate(() => { | |
// ... 1 more lines ... | |
// | |
// --- | |
// | |
// It is an uncommon case that a Promise has two "then" handler. This debug script checks how many handlers are attached and | |
// shows the error message. Usually it should be indicative and helpful when debugging. | |
// |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment