The JavaScript Event Loop: Explained

What’s this post about?

With JavaScript approaching near-ubiquity as the scripting language of the web browser, it benefits you to have a basic understanding of its event-driven interaction model and how it differs from the request-response model typically found in languages like Ruby, Python, and Java. In this post, I’ll explain some core concepts of the JavaScript concurrency model, including its event loop and message queue in hopes of improving your understanding of a language you’re probably already writing but perhaps don’t fully understand.

Who is this post for?

This post is aimed at web developers who are working with (or planning to work with) JavaScript in either the client or the server. If you’re already well-versed in event loops then much of this article will be familiar to you. For those of you who aren’t, I hope to provide you with a basic understanding such that you can better reason about the code you’re reading and writing day-to-day.

Non-blocking I/O

In JavaScript, almost all I/O is non-blocking. This includes HTTP requests, database operations and disk reads and writes; the single thread of execution asks the runtime to perform an operation, providing a callback function and then moves on to do something else. When the operation has been completed, a message is enqueued along with the provided callback function. At some point in the future, the message is dequeued and the callback fired.

While this interaction model may be familiar for developers already accustomed to working with user interfaces – where events like “mousedown,” and “click” could be triggered at any time – it’s dissimilar to the synchronous, request-response model typically found in server-side applications.

Let’s compare two bits of code that make HTTP requests to www.google.com and output the response to console. First, Ruby, with Faraday:

The execution path is easy to follow:

  1. The get method is executed and the thread of execution waits until a response is received
  2. The response is received from Google and returned to the caller where it’s stored in a variable
  3. The value of the variable (in this case, our response) is output to the console
  4. The value “Done!” is output to the console

Let’s do the same in JavaScript with Node.js and the Request library:

A slightly different look, and very different behavior:

  1. The request function is executed, passing an anonymous function as a callback to execute when a response is available sometime in the future.
  2. “Done!” is immediately output to the console
  3. Sometime in the future, the response comes back and our callback is executed, outputting its body to the console

The Event Loop

The decoupling of the caller from the response allows for the JavaScript runtime to do other things while waiting for your asynchronous operation to complete and their callbacks to fire. But where in memory do these callbacks live – and in what order are they executed? What causes them to be called?

JavaScript runtimes contain a message queue which stores a list of messages to be processed and their associated callback functions. These messages are queued in response to external events (such as a mouse being clicked or receiving the response to an HTTP request) given a callback function has been provided. If, for example a user were to click a button and no callback function was provided – no message would have been enqueued.

In a loop, the queue is polled for the next message (each poll referred to as a “tick”) and when a message is encountered, the callback for that message is executed.

Theoretical Model

The calling of this callback function serves as the initial frame in the call stack, and due to JavaScript being single-threaded, further message polling and processing is halted pending the return of all calls on the stack. Subsequent (synchronous) function calls add new call frames to the stack (for example, function init calls function changeColor).

In this example, a message (and callback, changeColor) is enqueued when the user clicks on the ‘foo’ element and an the “onclick” event fires. When the message is dequeued, its callback function changeColor is called. When changeColor returns (or an error is thrown), the event loop continues. As long as function changeColor exists, specified as the onclick callback for the ‘foo’ element, subsequent clicks on the element will cause more messages (and associated callback changeColor) to become enqueued.

Queuing Additional Messages

If a function called in your code is asynchronous (like setTimeout), the provided callback will ultimately be executed as part of a different queued message, on some future tick of the event loop. For example:

Due to the non-blocking nature of setTimeout, its callback will fire at least 0 milliseconds in the future and is not processed as part of this message. In this example, setTimeout is invoked, passing a callback function g and a timeout of 0 milliseconds. When the specified time elapses (in this case, almost instantly) a separate message will be enqueued containing g as its callback function. The resulting console activity would look like: “foo”, “baz”, “blix” and then on the next tick of the event loop: “bar”. If in the same call frame two calls are made to setTimeout – passing the same value for a second argument – their callbacks will be queued in the order of invocation.

Web Workers

Using Web Workers enables you to offload an expensive operation to a separate thread of execution, freeing up the main thread to do other things. The worker includes a separate message queue, event loop, and memory space independent from the original thread that instantiated it. Communication between the worker and the main thread is done via message passing, which looks very much like the traditional, evented code-examples we’ve already seen.

Web Workers

First, our worker:

Then, the main chunk of code that lives in a script-tag in our HTML:

In this example, the main thread spawns a worker and registers the logResult callback function to the its “message” event. In the worker, the reportResult function is registered to its own “message” event. When the worker thread receives the message from the main thread, the worker enqueues a message and corresponding reportResult callback. When dequeued, a message is posted back to the main thread where a new message is enqueued (along with the logResult callback). In this way the developer can delegate CPU-intensive operations to a separate thread, freeing the main thread up to continue processing messages and handling events.

A Note on Closures

JavaScript’s support for closures allow you to register callbacks that, when executed, maintain access to the environment in which they were created even though the execution of the callback creates a new call stack entirely. This is particularly of interest knowing that our callbacks are called as part of a different message than the one in which they were created. Consider the following example:

In this example, the changeHeaderDeferred function is executed which includes variable header. The function setTimeout is invoked, which causes a message (plus the changeHeader callback) to be added to the message queue approximately 100 milliseconds in the future. The changeHeaderDeferred function then returns false, ending the processing of the first message – but the header variable is still referenced via a closure and is not garbage collected. When the second message is processed (the changeHeader function) it maintains access to the header variable declared in the outer function’s scope. Once the second message (the changeHeader function) is processed, the header variable can be garbage collected.

Takeaways

JavaScript’s event-driven interaction model differs from the request-response model many programmers are accustomed to – but as you can see, it’s not rocket science. With a simple message queue and event loop, JavaScript enables a developer to build their system around a collection of asynchronously-fired callbacks, freeing the runtime to handle concurrent operations while waiting on external events to happen. However, this is but one approach to concurrency. In the second part of this article I’ll compare JavaScript’s concurrency model with those found in MRI Ruby (with threads and the GIL), EventMachine (Ruby), and Java (threads).

Additional Reading



This entry was posted in Everything Else. Bookmark the permalink.
  • Rob Pak

    Good stuff, thanks for the write up.

  • Nazar Gargol

    Hi and thanks for great explanation. Just a little typo I’ve found: “the provided callback will ultimately will be executed”, too much ‘will’ :)

    • Erin

      Thanks Nazar! Just fixed the typo.

  • Alan

    Thanks, you successfully nailed it! I like the graphics (seriously).

  • kryptogoloc

    If Javascript had proper threading we would not need asynchronous handlers. Async handlers at first seem very good but it easily devolves into a terrible stew of race conditions, continuations and various ‘continuation / promise’ frameworks. All of these problems would evaporate with a proper threading model.

    Javascript broken-ness is most evident here. The mental model the programmer has is synchrony, and synchrony is best implemented with threads and mutexes.

    • Erin

      Hi kryptogoloc. You said, “The mental model the programmer has is synchrony.” Which programmers are you referring to? All of us?

    • Richard Klancer

      “If Javascript had proper threading.”… I’m sorry, but I and many others could hardly disagree more strongly.

      Javascript’s (de facto, if not de jure) single-threaded nature provides an absolutely essential invariant: between the beginning of my callback and the end, nothing else can mess with my state. See http://calculist.org/blog/2011/12/14/why-coroutines-wont-work-on-the-web/ :

      “You can go ahead and leave things in a funky intermediate state for as long as you like, and as long as you stitch everything back up in time for the next spin of the event loop, no other code can run in the meantime. That means you can be sure that while your object is lying in pieces on the floor, nobody else can poke at it before you put it back together again.”

      Certain types of race conditions are possible of course–you might depend on some item of state in callback 2, forgetting that callbacks between callback 1 and callback 2 might affect it. But I believe these indicate fundamental errors in your code’s logic and/or modularity, rather than being something that happens because you forgot to be sufficiently paranoid, at every single line of code in your program, about who else might swoop in and modify shared memory.

      In Javascript implementations, you never have to worry about this kind of race condition: “Wait! What if a context switch happens *right here*, after I decrement this but before I got around to incrementing that. Let’s see, does that operator translate to a single machine instruction, or…”

      Certainly my experience is that, once the *correct* mental model establishes itself firmly in your mind, it starts to feel not only easy, but somehow essentially right, to use callbacks bound to a closure in order to capture state about one particular ongoing event in the world (a particular sequence of clicks, an http request and response, intermediate results coming in from a web worker) while having the luxury of ignoring what might or might not happen in the rest of the world. Closures-with-callbacks looks more complex at first than code that blocks on I/O to “look sync”, but it makes for code that’s easy to reason about, and in which you don’t have to constantly defend against potential failures of transactionality.

  • http://stackoverflow.com/users/425275/ime-vidas Šime Vidas

    Note that the JavaScript language (ECMAScript) does not define any U/I mechanisms. Rather, it’s the host environment that provides these. Hence, the event loop is provided by web browsers (and other environments like Node), so a better name would be “the browser’s event loop”.

    • Erin

      Hi Šime.

      Since we’re getting all pedantic: JavaScript (the language) is an implementation of ECMA-262 (a specification). If you’re saying that an event loop is not part of the ECMA-262 specification – I agree with you. However, all modern JavaScript engines (that I’m aware of) provide some sort of an event loop. I don’t think it’s of practical value to the reader that I make that distinction up front.

      • http://stackoverflow.com/users/425275/ime-vidas Šime Vidas

        But is the event loop built-in into JavaScript engines, in practice? Or is it provided as a separate component? If the latter case is true, then “JavaScript event loop” may not be the optimal name for this. I don’t think that discussing optimal names for these techs is pedantism, and neither should you.

  • Pingback: MNCC : Weekly Link Digest

  • Richard Klancer

    Have you thought about writing up something about the HTML5 Working Group’s standardization of the (browser) event loop, and the formalization of distinct “task sources” for DOM manipulation, user interaction, networking, etc? http://www.w3.org/TR/html5/webappapis.html#event-loops

    (I have not read up on this subject, yet)

    Relatedly, TC39 appears to be thinking about standardizing some event loop semantics in the Javascript standard itself: http://esdiscuss.org/topic/es6-es7-es8-and-beyond-a-proposed-roadmap.

  • http://www.akitaonrails.com/ AkitaOnRails

    Nice introduction to asynchronous calls. I know the intention was not to bash Ruby or anything, so don’t take this comment as a rebuttal. Just so it’s clear for all the readers, Ruby can do async calls as well. The initial example uses the Faraday gem to make a synchronous, blocking HTTP call, and wait for the response. Faraday can use other gems, one of them being Typhoeus, which uses libcurl underneath. And it so happens that you can do asynchronous, non-blocking HTTP calls in Ruby with Faraday+Typhoeus as well (http://blog.carbonfive.com/2013/03/15/parallel-http-requests-with-faraday-and-typhoeus/). And this is not the only options, there are several other ways to do async, non-blocking HTTP calls in Ruby, one other example being with Eventmachine and EM-HTTP.

    • Erin

      Hey Akita,

      You’re absolutely right – there are several ways to write asynchronous code in Ruby. While some of those projects are growing in popularity these days (EventMachine, for instance), nearly all of the Rails-based applications I’ve come across lately have been doing I/O synchronously. I’m not sure why the community hasn’t yet embraced asynchronous I/O. Any thoughts? Sampling error?

      • http://twitter.com/mwynholds Michael Wynholds

        I think the Ruby community is now starting to embrace async I/O, because companies are now starting to use (or trying to use) Ruby in performance intensive applications.

        Regarding the approaches mentioned here:

        1. Typheous (and other async gems)
        I love Typheous, but it’s a targeted solution. It’s not practical to force developers to find individual gems with async support for every type of I/O operation they need to do.

        2. EventMachine
        I also really like EventMachine, but I don’t think it will be the way Rubyists go async. It puts the responsibility of writing Fiber-aware code on either the app developer or else all I/O-based gem developers, and that’s not a feasible approach either. Async support has to be at the language (or at least framework) level.

        In my opinion, Rubyists are moving towards threads, and specifically using an Actor model. This is actually what Web Workers are, so there is a parallel in JavaScript.

        And I think this is the right way to go, personally.

  • Pingback: Links & reads for 2013 Week 44 | Martin's Weekly Curations

  • bowerbird

    > With JavaScript approaching near-ubiquity
    > as the scripting language of the web browser,

    i am confused by statements like this,
    which i seem to be hearing quite often.

    in my understanding, javascript is the
    _only_ way to put code in a web-page,
    so it’s no surprise it’s “near-ubiquity”…

    so please, if there is some other way
    that i can run code inside a web-page,
    let me know, because i don’t really like
    javascript all that much (in fact, i dislike
    it in large part), i’m just using it because
    i thought i had to. but if i had a choice…

    well, i’d certainly consider that choice…

    -bowerbird

    • http://twitter.com/mwynholds Michael Wynholds

      i’m sure there are more fringe cases, but two that come to mind immediately:
      1. flash
      2. vbscript runs in ie (at least older versions, not sure about newer)

      • bowerbird

        no future in either of those, as you know.
        so my understanding is perfectly correct.

        plus now i know why people toss in that
        small hedge, so thank you for explaining.

        -bowerbird

        • http://twitter.com/mwynholds Michael Wynholds

          well, maybe your understanding is correct. but when you say that “javascript is the _only_ way to…”, it seems like you are making a point to emphasize “only” as a matter of fact. and of course, it’s not a fact.

          i would also point out that while flash may seem on the down-swing, it’s not dead yet by any means.

          however if you’re actually interested in technologies that have a future (in your opinion), then some more come to mind:

          CoffeeScript
          ASM

          you can write and run coffeescript directly in the browser. it’s arguable whether you should differentiate that from JavaScript.

          asm runs native in some browsers, and is a subset of javascript.

          anyway, it sort of seems like semantics, but if someone writes a blog post and says javascript is the only choice for writing code in a browser, i guarantee they will get people telling him about all these other options.

          • bowerbird

            touche.

            i should have said “only _practical_ way”… :+)

            -bowerbird

    • nnnnnn321

      I can’t speak for the author, but I read the sentence you quoted as meaning “Javascript is approaching near ubiquity because it is the scripting language of the browser”. That is to say that nearly everyone (or every programmer, anyway) has been exposed to JS to some degree.

      As for having a choice, there are tools around that let you write code in other languages and cross-compile back to JS for deployment. Obviously this doesn’t let you do anything that JS can’t do natively, but it does give you a wide selection of syntax styles. See https://github.com/jashkenas/coffee-script/wiki/List-of-languages-that-compile-to-JS

  • Pingback: Weekly techy stuff by Kelisto blog

  • Luka

    Brilliant article! thanks for this

  • Bolshchikov

    setTimeout(fn, 0) is kind of confusing because, even passing 0, it won’t invoked in the same tick. JS engines of different browsers have the minimum amount for setTimeout parameter (in Chrome it’s 4ms). So in Chrome setTimeout(fn, 0) === setTimeout(fn, 4).
    But, there’s a setImmediate(fn) that will be called when the call stack is empty but before next tick of the event loop

    • Erin

      Hey Bolshchikov,

      I didn’t know about window.setImmediate! According to MDN (https://developer.mozilla.org/en-US/docs/Web/API/Window.setImmediate), it seems to be available only in IE10. Can you describe a case for its use?

      Regarding setTimeout(fn, 0): I think the API is a bit misleading. The semantic promise of setTimeout is “call this function n-milliseconds from now,” but what’s actually happening is that you’re asking the engine to enqueue a message (and thus your callback) n-milliseconds from the moment you called it. If some CPU-heavy stuff causes the current tick of the event loop to last for 10000 milliseconds, your function won’t be called for _at least_ 10000 milliseconds. Grokking this behavior requires that the programmer understand that, behind the scenes, a single-threaded event loop / queue mechanism is responsible for the running of their code.

      • Bolshchikov

        I totally agree with you about setTimeout. Its time is confusing.
        Regarding the setImmediate, I didn’t have a use case yet such that I would need it. As far as I understood, it was added exactly to solve the setInterval(fn, 0) case, because it’s not accurate.

  • Bolshchikov

    The best explanation of JS concurrency model I’ve read so far. Can you recommend any other advanced reading materials regarding this topic?

  • Pingback: Hanging up on Callbacks: Generators in ECMAScript 6 | The Carbon Emitter

  • Pingback: javascript事件轮询(event loop)详解 | Web开源笔记-专注Web开发技术,分享Web开发技术与资源

  • Pingback: Taking Advantage of Multi-Processor Environments in Node.js | The Carbon Emitter

  • Pingback: (转) The JavaScript Event Loop: Explained | Web开源笔记-专注Web开发技术,分享Web开发技术与资源

  • Pingback: HR Week 4: Servers & Databases | Emily D

  • http://laike9m.com/ laike9m

    Amazing

  • Nozo.mk

    In the example Queuing Additional Messages :
    if I replace the the line
    setTimeout(g, 0);
    with my custom event :
    var evt = document.createEvent(“Event”);
    evt.initEvent(“myEvent”,true,true);
    document.addEventListener(“myEvent”,g,false);
    document.dispatchEvent(evt);

    then the function g() is immediately executed, it is not waiting until the next loop tick . Why ?
    Thanks