Search code examples
javascriptsettimeoutanonymous-functionasynccallback

Where are anonymous callback functions to setTimeout stored?


I have a simple program that looks like this:

console.log("Start of the program execution!");

setTimeout(() => {
    console.log("I ran after a second!");
}, 1000);

console.log("The end of the file!");

Then, the question I have is: Where does the anonymous function live while the timer is running and the function is not put onto the call stack?

If I had a normal function definition, the code would look like this:

function doSth()
{
    console.log("I ran after a second!");
}

console.log("Start of the program execution!");

setTimeout(doSth, 1000);

console.log("The end of the file!");

then, the function doSth() would still live in the global memory after the call stack became empty and thus the setTimeout() could use its reference to call it after the set time. This arrangement makes perfect sense to me. The problems arise when I use an anonymous function and make some observations.

For some observations, I first put the debugger on line 5 of the program; the console.log("The end of the file!");. Then I take a look at the Scope of the Developer Tools. The anonymous function is nowhere to be seen.

For the next observation, I put the debugger on the console.log() inside the setTimeout() function, which is line 3 of the program. Then indeed the call stack has the anonymous function on it when we are running it.

Thus, these two observations bring forward the confusion for me. If the anonymous function was not present in the global memory AFTER the setTimeout() was called, then how can it be pushed to the call stack when the setTimeout() finishes its job? Does the function "go with" the setTimeout() to the webAPIs domain and then come back from there?


Solution

  • The HTML Standard defines the initialisation steps for setTimeout. It includes the definition of a task in step 5:

    1. Let task be a task that runs the following substeps:

          [1. ... 2. ...]

      1. If handler is a Function, then invoke handler

    In other words, the task created by setTimeout holds a reference to the callback (handler) inside a task object -- no matter whether it is anonymous or named. By general principle in JavaScript, whenever an object has at least one reference, it will not be candidate for garbage collection.

    When the timeout period has expired, a step is executed -- called the completionStep --, described in step 11 in the same procedure:

    1. Let completionStep be an algorithm step which queues a global task on the timer task source given global to run task.

    The actual execution of that algorithm step is described in run steps after timeout.

    But the essential thing is that the agent (host, webbrowser) that provides setTimeout has a reference to the callback function during the time of calling setTimeout and the eventual execution of the callback.

    Evaluation order

    When the engine evaluates an expression that has a function call in it (in this case a call of setTimeout), then first the arguments are evaluated and only then that function is called. In your example code the order of evaluation is:

    1. The callback function (object):

      () => {
          console.log("I ran after a second!");
      }
      

      It is in this step that a function object is allocated. Then:

    2. 1000

      This happens to be a simple primitive value.

    3. The setTimeout expression:

      setTimeout(() => {
          console.log("I ran after a second!");
      }, 1000);
      

      At this point setTimeout is called, and it gets the reference to the function object created in step 1, and the primitive value 1000.

    It is not setTimeout that creates this callback object in memory. This happens by normal evaluation order and is not specific to setTimeout.

    If setTimeout would not do anything with the first argument it gets, then when it returns, the callback function object created in step 1 would no longer have a reference to it. Its memory could be freed any time the garbage collector runs. But as setTimeout copies the function reference into non-local memory (in a task object), that callback function remains available for later execution: it has a reference to it that outlives the setTimeout call.