Accurate call sites

Call sites API does not support source maps, while exception stack does.

Can we find out what user function called our function (in a library)? For example, imagine two files, one exporting a function foo and another one calling it foo(). Can we accurately know inside function foo who called it?

foo.js
1
2
3
4
module.exports = function foo () {
console.log('inside foo')
// who called me?
}
call-foo.js
1
2
3
4
5
6
const foo = require('./foo')
function callFoo() {
console.log('calling foo from callFoo')
foo()
}
callFoo()
1
2
3
$ node call-foo.js
calling foo from callFoo
inside foo

Turns out we can! Node's V8 engine has an stack trace API for getting callsite information. We can easily get this information using callsites. Inside our function foo we can get the file and the line number of the caller.

foo.sj
1
2
3
4
5
6
7
8
9
10
11
const callsites = require('callsites')
const relativeTo = require('path').relative.bind(null, process.cwd())
module.exports = function foo () {
console.log('inside foo')
// who called me?
const caller = callsites()[1]
console.log('caller %s line %d column %d',
relativeTo(caller.getFileName()),
caller.getLineNumber(),
caller.getColumnNumber())
}
1
2
3
4
$ node call-foo.js
calling foo from callFoo
inside foo
caller call-foo.js line 4 column 3

Which tells the right information; foo() call was from line 4 (line numbers are reported starting with line 1) of the file call-foo.js.

Transpiled code

Yet, there is a problem with using the above call sites if the code has been modified by the loader. For example, we can insert a line at the beginning of call-foo.js before Node evaluates it. I will use my node-hook to register a loading callback. It will only modify call-fool.js for simplicity.

add-log-line.js
1
2
3
4
5
6
7
8
9
const hook = require('node-hook')
function addLogLine (source, filename) {
if (filename.endsWith('call-foo.js')) {
console.log('inserting first line into call-foo.js')
return 'console.log("first line in call-foo.js")\n' + source
}
return source
}
hook.hook('.js', addLogLine)

We can register the hook when running Node

1
2
3
4
5
6
$ node -r ./add-log-line.js call-foo.js
inserting first line into call-foo.js
first line in call-foo.js
calling foo from callFoo
inside foo
caller call-foo.js line 5 column 3

Hmm, the call site now reports that the call foo() happened at line 5! The call site logic does NOT take into account the modifications done by the code loading hooks, any code transpiled by Babel for example will have incorrect source line and column numbers.

ES6 modules

Let us change our foo.js to export the function "foo" using ES6 module syntax instead of following CommonJS standard.

foo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict'
const callsites = require('callsites')
const relativeTo = require('path').relative.bind(null, process.cwd())
export function foo () {
console.log('inside foo')
// who called me?
const caller = callsites()[1]
console.log('caller %s line %d column %d',
relativeTo(caller.getFileName()),
caller.getLineNumber(),
caller.getColumnNumber())
}
// use: const foo = require('./foo').foo

We cannot run this function directly, thus we need to transpile it on the fly, for example by using babel-node command.

package.json
1
2
3
4
5
{
"scripts": {
"es6": "babel-node call-foo.js"
}
}
1
2
3
4
5
$ npm run es6
babel-node call-foo.js
calling foo from callFoo
inside foo
caller call-foo.js line 6 column 3

Due to source on the fly transpile, the foo() call inside "call-foo.js" file is reported incorrectly at line 6, while in reality it happens at line 5.

Accurate numbers in the error stack

Surprisingly if you throw an exception from the transpiled code, the stack shows the correct original numbers. Modify foo.js to throw an Error after printing the call site.

foo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict'
const callsites = require('callsites')
const relativeTo = require('path').relative.bind(null, process.cwd())
export function foo () {
console.log('inside foo')
// who called me?
const caller = callsites()[1]
console.log('caller %s line %d column %d',
relativeTo(caller.getFileName()),
caller.getLineNumber(),
caller.getColumnNumber())
throw new Error('on purpose')
}
1
2
3
4
5
6
7
8
9
10
11
12
$ npm run es6
babel-node call-foo.js

calling foo from callFoo
inside foo
caller call-foo.js line 6 column 3
foo.js:14
throw new Error('on purpose');
^
Error: on purpose
at foo (foo.js:12:9)
at callFoo (call-foo.js:5:3)

Weird, but Node environment has the right line information for the original untranspiled location at callFoo (call-foo.js:5:3), probably there is a source map somewhere. Can we verify this?

Transforming source code

We can see the loaded code after all transformations. We can do this by loading the code and running it through the loading hook callback function ourselves.

You can see the code has been transformed by inspecting the exception call stack. See the line Object.require.extensions [as .js] below.

1
2
3
4
5
6
7
8
9
caller Error: on purpose
at foo (/foo.js:13:11)
at callFoo (/call-foo.js:5:3)
at Object.<anonymous> (/call-foo.js:7:1)
at Module._compile (module.js:570:32)
at loader (/node_modules/babel-register/lib/node.js:144:5)
at Object.require.extensions.(anonymous function)
[as .js] (/node_modules/babel-register/lib/node.js:154:7)
at Module.load (module.js:487:32)

Let us see how the "call-foo.js" looks transformed. We can just grab the current transform function and give it a fake module to grab the transformed code

foo.js
1
2
3
4
5
6
7
8
9
10
// ...
const filename = resolve('./call-foo.js')
const transform = Module._extensions['.js']
const fakeModule = {
_compile: source => {
console.log('transformed code')
console.log(source)
}
}
transform(fakeModule, filename)

Which prints almost the same source code as the original one, except it has a source map at the end and an empty line after use strict line.

1
2
3
4
5
6
7
8
9
10
transformed code
'use strict';

var foo = require('./foo').foo;
function callFoo() {
console.log('calling foo from callFoo');
foo();
}
callFoo();
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjo...

That blank like after 'use strict' is the source of the confusion! The call site V8 API could not use the included source map to report accurate line numbers, but the exception stack could.

How is this useful?

Knowing the accurate caller location can be useful for creating simple and powerful utilities. For example the framework-agnostic snap-shot library uses the above approach to get the caller information to find which test callback function called it. This allows "magic" zero-configuration use without relying on runner context or additional arguments.

1
2
3
4
5
const snapshot = require('snap-shot')
it('knows which test calls it', () => {
snapshot({foo: 'bar'})
// snapshot knows 'know which test calls it' test called it
})

I have found the difference between locations reported using call sites and exception stack for ES6 modules during testing for that project. Luckily it is simple to grab the line numbers and filenames from the exception stack.

foo.js
1
2
3
4
5
6
7
8
export function foo () {
try {
throw new Error('on purpose')
} catch (e) {
console.log('caller', e.stack.split('\n')[2])
// do more parsing if necessary
}
}