Let’s say you need to find out how your function was called, either for in app debugging or some other purpose. A JavaScript Error has a non standard property (stack) that contains some info about how to find out. This is supported by V8, so since Apps Script is running V8 we can use it.

Motivation

It’s not a great idea to modify a functions behavior depending on who called it, but for Sheets Workbook functions – converted to Apps Script I needed to find a way of handling the Sheets function Arrayformula, which modifies the behavior of other functions when used as a function from sheets. I already have a way of selecting a default behavior for a function as described in  JavaScript snippet to create a default method for an object but I also need to know whether it’s under the control of Arrayformula or not. This is not great practice, but it’s necessary to be able to exactly emulate how Arrayformula modifies the behavior of the function it’s controlling.

The stack

For demo purposes. I’ll use this simple stack structure. The objective here is that in function d, I want to find out who called me.

function a() {
  b()

  function b() {
    c()
  }
}
function c() {
  d()
}

function d() {
  const stacked = stacker()
  console.log('got called like this', stacked)
  console.log('immediate caller was', stacked[0].name)
}
demo

Creating a stack

It can be extracted from an new Error instance, as below. You can see that everything we need is there, so we’ll just need a few regexes to clean it up

  // check we're running in supported v8 environment
  if (!Error.captureStackTrace) {
    throw new Error('stack trace not supported - only works on V8')
  }

  // create an instance of an error
  const instance = new Error();

  // the stack trace looks like this
  /*
Error
    at stacker (test:109:20)
    at d (test:96:19)
    at c (test:92:3)
    at b (test:88:5)
    at a (test:85:3)
    at __GS_INTERNAL_top_function_call__.gs:1:8
  */
  const { stack } = instance
stack trace

Anonymous functions

Anonymous functions have a slightly different trace format so we’re going to need to cater for that too. Consider this version of the c function in the test

function c() {
  (()=>d())()
}
anonymous function

Anonymous stack trace

	Error
    at stacker (test:109:20)
    at d (test:96:19)
    at test:92:8
    at c (test:92:12)
    at b (test:88:5)
    at a (test:85:3)
    at __GS_INTERNAL_top_function_call__.gs:1:8
anonymous stack trace

Object methods

These have a slightly different format too

function a() {
  const o = {
    b: ()=> c()
  }
  o.b()
}
function c() {
  (()=>d())()
}

function d() {
  const stacked = stacker()
  console.log('got called like this', stacked)
  console.log('this is me', stacked[0])
  console.log('this is who called me', stacked[1])
}
called from an object method

Object method stack trace

Error
    at stacker (test:109:20)
    at d (test:95:19)
    at test:91:8
    at c (test:91:12)
    at Object.b (test:86:13)
    at a (test:88:5)
    at __GS_INTERNAL_top_function_call__.gs:1:8
object method stack trace

Property get and set

And again, a little different

function a() {
  const o = {
    get b (){
      return c()
    }
  }
  o.b()
}
function c() {
  (()=>d())()
}

function d() {
  const stacked = stacker()
  console.log('got called like this', stacked)
  console.log('this is me', stacked[0])
  console.log('this is who called me', stacked[1])
}
property get

Property get stack trace. A property  set gives the same trace, except Object.set rather than Object.get

Info	Error
    at stacker (test:111:20)
    at d (test:97:19)
    at test:93:8
    at c (test:93:12)
    at Object.get b [as b] (test:87:14)
    at a (test:90:5)
    at __GS_INTERNAL_top_function_call__.gs:1:8
property get stack trace

Instantiated class

Similar to as Object method and property set and get except this time you get the class name rather than ‘Object’

 

 

Cleaning up the stack

We need a regex to cater for this variety of stack traces, preferably one that deconstructs into each of the variants above, which can be expressed as

I’d like to organize that into an object like this.
{
	name, 		// the caller name or 'anonymous'
	className, 	// the class name or 'Object'
	type, 		// get, set or 
	file,		// the script file
	line,		// the line number in the script file
	position,	// the position in the line in the script file
	depth		// the depth where the function that calls stacker is 0
}
	
result structure
It’s a pretty unstructured thing, so we have to get a bit hacky with the regexs, as below

  // the file info
  const fx = /\(?(\w+):(\w+):(\w+)\)?$/
  // to get rid of the file info
  const dx = /\s+(\(?\w+:\w+:\w+\)?)$/
  // tp split the remainder
  const sx = /\s*?at\s+(\w+)?[\s\.]*(\w+)?[\s\.]*(\w+)?/
stacker regexes

The result

The result is an array of information about the callers. Since this is going to be a generalized function (stacker) , we will slice off the call to the stacker itself as well as the Apps Script run time initialization – none of which are of interest

[{
	"name": "d",
	"className": null,
	"type": null,
	"file": "test",
	"line": 103,
	"position": 19,
	"depth": 0
}, {
	"name": null,
	"className": null,
	"type": null,
	"file": "test",
	"line": 99,
	"position": 8,
	"depth": 1
}, {
	"name": "c",
	"className": null,
	"type": null,
	"file": "test",
	"line": 99,
	"position": 12,
	"depth": 2
}, {
	"name": "m",
	"className": "E",
	"type": null,
	"file": "test",
	"line": 89,
	"position": 12,
	"depth": 3
}, {
	"name": "a",
	"className": null,
	"type": null,
	"file": "test",
	"line": 95,
	"position": 12,
	"depth": 4
}]
result

 

Stacker function

Now this can be called from anywhere to reveal who the caller is.

const stacker = () => {
  // check we're running in supported v8 environment
  if (!Error.captureStackTrace) {
    throw new Error('stack trace not supported - only works on V8')
  }

  // create an instance of an error
  const instance = new Error();
  const { stack } = instance

  // dump the call from apps script rt and the Error introduction
  // we should end up with a an array of object names, callers and files and line numbers
  
  // best to do the regex in bits
  // the file info
  const fx = /\(?(\w+):(\w+):(\w+)\)?$/
  // to get rid of the file info
  const dx = /\s+(\(?\w+:\w+:\w+\)?)$/
  // tp split the remainder
  const sx = /\s*?at\s+(\w+)?[\s\.]*(\w+)?[\s\.]*(\w+)?/

  return stack.split("\n")
    .slice(2, -1)
    .map((m,i) => {
      // the file info
      const f = m.match(fx)
      // the remainder
      const d = m.replace(dx,'')
      // sort out the caller object name etc.
      const s = d.match(sx) 
      // if none, then its anonymous
      // if 1 its a straight function
      // if 2 then its something like object.name
      // if we have all 3 then its something like object.get name

      return {
        name: (s && (s[3] || s[2] || s[1])) || null,
        className: (s && (s[3] || s[2])) ? s[1] : null,
        type: s && s[3] ? s[2] : null,
        file:  f[1],
        line: parseInt(f[2]),
        position: parseInt(f[3]),
        depth: i
      }
    })

}
stacker

and it can be used like this from any function to clean up the call stack

function d() {
  const stacked = stacker()
  console.log('got called like this', stacked)
  console.log('this is me', stacked[0])
  console.log('this is who called me', stacked[1])
}
stacker usage