0

Edit: I have removed the original code and replaced it with generalized code that anyone can run themselves to reproduce my problem:

var StripeCheckout = {
    configure: function(obj){
        obj.token();
    }
};

StripeCheckout.configure({
    token: function(token) {
        var request = new XMLHttpRequest();
        request.open('POST', 'http://localhost:3000/process-order');
        request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
        request.onreadystatechange = function() {
            debugger; // right here is the problem
        };
        request.send();
    }
});

For me, inside that anonymous function where the debugger statement is, request is mysteriously not defined and attempting to access it is a ReferenceError. That really mystifies me.

This is the weird part. If I create a throwaway variable defined at the top level, and set throwaway = request; right after creating the request object, then inside the onreadystatechange handler throwaway is defined and everything is okay, even though request is not defined.

I just tested it, and I can also access it as this at the point of the debugger statement, as expected. What gives? Why is request not defined?

temporary_user_name
  • 35,956
  • 47
  • 141
  • 220
  • What happens if you replace `debugger;` with `console.log(response);`? – Boji Nov 23 '18 at 14:07
  • Dont always trust debuggers. Trust what you know of the language. I've seen plenty of cases where for instance, the Edge debugger would lie about state. – plalx Nov 26 '18 at 02:44
  • 2
    Exact duplicate of [Why does Chrome debugger think closed local variable is undefined?](https://stackoverflow.com/q/28388530/1048572) – Bergi Nov 26 '18 at 07:22

2 Answers2

1

The reference to request is a reference into the execution context surrounding the inner function, which is a heap-allocated structure implementing part of the closure, and is subject to garbage collection.

As explained here, the V8 JavaScript engine (which is used by Chrome and Node.JS, among other applications, to implement JavaScript) does not create a closure if there is no reference to it. (This is an optimization.) Therefore, by the time you hit the debugger, the reference to request has been lost, which is why you get ReferenceError rather than undefined. Note that the behavior is different in Safari 11, which uses JavaScriptCore from WebKit (rather than V8) to implement JavaScript.

If you add a reference to request inside the inner function, then the closure will be created by V8 and you will have access to in inside the debugger.

Examples

Run this code in Chrome (or as I have, in Opera, which also uses V8) and you will see a Closure listed in the Scope chain.

var StripeCheckout = {
  configure: function(obj) {
    return obj.token();
  }
};

StripeCheckout.configure({
  token: function(token) {
    var request = new XMLHttpRequest();
    request.open('POST', 'https://jsonplaceholder.typicode.com/posts');
    request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
    request.onreadystatechange = function() {
      // include the console.log statement 
      // and everything works as expected.
      console.log('req.rs:', request.readyState);
      debugger; // right here is the problem
    };
    request.send();
  }
});

Chrome debugger showing closure

Comment out the console.log statement, which is the only reference to the closure, and the closure itself disappears (with V8).

Closure not present with V8

However, when you run the same code (without the reference) in Safari, which uses JavaScriptCore from WebKit instead of V8, and the closure is still present with request defined, and the debugger works as expected.

Closure present in Safari

Old Pro
  • 24,624
  • 7
  • 58
  • 106
0

JavaScript functions are closures. They capture the variables in the surrounding scope that they need in order to work, but no more. Since you are not referencing request in the actual code of your function, it is not captured by JavaScript because your function doesn't appear to need it. If you include code that references request, then it will be defined:

var request = new XMLHttpRequest();
request.open('POST', 'http://localhost:3000/process-order');
request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
request.onreadystatechange = function() {
    console.log(request.toString());
    debugger;
};
request.send();

If you run this snippet with your browser's developer tools window open, it will stop on the debugger statement, and request will be defined.

enter image description here

On the other hand, your original snippet may fail because request is not referenced in the function, so the closure does not include it. By the time you try to look at the value of request, it may have already been cleaned up.

The reason that this happens is for performance: if JavaScript functions just captured every single variable in scope, then many variables would never be released, and your web browser would use even more memory and take a performance hit due to having to keep track of all of those variables for longer.

laptou
  • 6,389
  • 2
  • 28
  • 59