Given the following example, why is outerScopeVar
undefined in all cases?
var outerScopeVar; var img = document.createElement('img'); img.onload = function() { outerScopeVar = this.width; }; img.src = 'lolcat.png'; alert(outerScopeVar);
var outerScopeVar; setTimeout(function() { outerScopeVar = 'Hello Asynchronous World!'; }, 0); alert(outerScopeVar);
// Example using some jQuery var outerScopeVar; $.post('loldog', function(response) { outerScopeVar = response; }); alert(outerScopeVar);
// Node.js example var outerScopeVar; fs.readFile('./catdog.html', function(err, data) { outerScopeVar = data; }); console.log(outerScopeVar);
// with promises var outerScopeVar; myPromise.then(function (response) { outerScopeVar = response; }); console.log(outerScopeVar);
// with observables var outerScopeVar; myObservable.subscribe(function (value) { outerScopeVar = value; }); console.log(outerScopeVar);
// geolocation API var outerScopeVar; navigator.geolocation.getCurrentPosition(function (pos) { outerScopeVar = pos; }); console.log(outerScopeVar);
Why is undefined
output in all these examples? I don't need a solution, I want to know why this is happening.
Note: This is a specification question about JavaScript asynchronicity. Please feel free to improve this question and add more simplified examples that the community can agree on.
Fabrício's answer is very correct; but I would like to supplement his answer with something a little less technical, focusing on helping to explain the concept of asynchronicity through analogies.
analogy...
Yesterday, I was working on a job where I needed to get some information from my colleagues. I called him; the conversation went like this:
Having said that, I hung up the phone. Since I needed the information from Bob to complete my report, I left the report, went to get a cup of coffee, and then I read some emails. 40 minutes later (Bob is slow) Bob called back and gave me the information I needed. At this point, I continue working on my report since I have all the information I need.
Imagine if the conversation went like this;
I sat there and waited. and waited. and waited. 40 minutes. Do nothing but wait. Eventually, Bob gave me the information, we hung up, and I finished my report. But I lost 40 minutes of productivity.
This is asynchronous vs. synchronous behavior
This is exactly what happens in all the examples in our question. Loading images, loading files from disk, and requesting pages via AJAX are all slow operations (in the context of modern computing).
JavaScript allows you to register a callback function that will be executed when slow operations complete, instead of waiting for these slow operations to complete. But in the meantime, JavaScript will continue executing other code. The fact that JavaScript is executing other code while waiting for the slow operation to complete makes the behavior asynchronous. If JavaScript waits for an operation to complete before executing any other code, this will be synchronous behavior.
In the above code, we ask JavaScript to load
lolcat.png
, which is a sloooow operation. Once this slow operation is complete, the callback function will be executed, but in the meantime, JavaScript will continue processing the next line of code; i.e.alert(outerScopeVar)
.This is why we see the alert showing
undefined
; becausealert()
is processed immediately, not after the image is loaded.To fix our code, all we have to do is move the
alert(outerScopeVar)
code into the callback function. Therefore, we no longer need to declare theouterScopeVar
variable as a global variable.You will always see callbacks specified as functions because that's the only way in JavaScript to define some code but then execute it later.
So, in all our examples,
function() { /* Do Something */ }
is the callback; to fix all examples, all we have to do is change the required The code that operates the response is moved there!* Technically you can also use
eval()
, buteval()
is evil for that purposeHow to put callers on hold?
You may currently have some code similar to this;
However, we now know that
returning outerScopeVar
happens immediately; before theonload
callback function updates the variable. This causesgetWidthOfImage()
to returnundefined
and raise the alertundefined
.To fix this, we need to allow the function calling
getWidthOfImage()
to register a callback and then move the width alert inside that callback;...As before, note that we have been able to remove the global variable (
width
in this case).One word answer: Asynchronicity.
Preface
This topic has been repeated at least thousands of times on Stack Overflow. So first I'd like to point out some very helpful resources:
@Felix Kling's answer to "How to return a response from an asynchronous call?". See his excellent answer explaining synchronous and asynchronous processes, as well as the "Reorganizing your code" section.
@Benjamin Gruenbaum also put a lot of effort into explaining asynchronicity within the same thread.
@Matt Esch's answer to "Getting data from fs.readFile" also does a good job of explaining asynchronicity in a simple way.
Answers to Current Questions
Let's trace common behaviors first. In all examples,
outerScopeVar
is modified inside the function. The function is obviously not executed immediately; it is assigned or passed as a parameter. This is what we call a callback.The question now is, when will this callback be called?
It depends on the specific situation. Let's try tracing some common behaviors again:
img.onload
Might be called at some time in the future (if) the image has loaded successfully.setTimeout
may be calledat some time in the future
after the delay has expired and the timeout has not been clearTimeout canceled. Note: Even when using0
as the delay, all browsers have an upper limit on the minimum timeout delay (specified as 4 milliseconds in the HTML5 specification).$.post
may be called.fs.readFile
may be called at some time in the future when the file has been successfully read or an error has been thrown.In all cases, we have a callback that may be run at some time in the future. This "sometime in the future" is what we call Asynchronous Stream.
Asynchronous execution is pushed out of the synchronous process. That is, while the synchronous code stack is executing, the asynchronous code will forever execute. This is what JavaScript is single-threaded about.
More specifically, when the JS engine is idle - not executing a bunch of (a)synchronous code - it will poll for events that might trigger an asynchronous callback (e.g. timeout, network response received) and execute them one after another . This is considered an event loop.
That is, the asynchronous code highlighted in the hand-drawn red shape can only execute after all remaining synchronous code in its respective code block has executed:
In short, callback functions are created synchronously but executed asynchronously. You can't rely on the execution of an async function until you know it has executed, how do you do this?
It's really simple. Logic that relies on the execution of an async function should be initiated/called from within that async function. For example, moving
alert
andconsole.log
inside the callback function will output the expected results because the results are available at that time.Implement your own callback logic
Often, you need to perform more operations on the result of an async function, or perform different operations on the result depending on where the async function is called. Let's deal with a more complex example:
Note: I'm using
setTimeout
with a random delay as a generic async function; the same example works for Ajax, readFile, onload and any other asynchronous flow.This example obviously has the same problem as the other examples; it doesn't wait until the async function executes.
Let's solve this problem by implementing our own callback system. First, we get rid of that ugly
outerScopeVar
which is completely useless in this case. Then we add a parameter that accepts a function argument, our callback. When the asynchronous operation completes, we call this callback and pass the result. Implementation (please read the comments in order):Code snippet for the above example: