I am giving a talk at the University of Pennsylvania’s PL Club next week, and was given the opportunity to assign a bit of homework to the audience. This blog post contains the content of that homework.
Before my talk, I want you to appreciate some of the difficulties involved in writing and debugging web apps. If you have the time, I’d like you to run, tinker with, and think about some of the JavaScript code snippets below. It will help you understand where my research is coming from.
Basic JavaScript
For this section, I recommend you open your browser’s Developer Tools to the Console, which is a REPL. All modern browsers have this feature. There, you can follow along with the examples.
JavaScript is a high-level and dynamically typed programming language. When you write a web application, it must be written in or compile down to JavaScript.
It has some odd behavior due to implicit conversions:
There are two equality operators: ==
performs implicit conversions, ===
does not.
Objects are essentially maps from strings to properties, and you can define new properties at any time:
Objects can inherit properties from a prototype object:
Arrays are objects, too:
JavaScript has a loose definition of object types. To define a Dog
class, you define a function called Dog
and call it with new
. The result is an object that identifies as a Dog
:
We can muck with Dog
later to confuse instanceof
, though:
This is just a small taste of the many oddities of JavaScript. Due to its dynamism, it is very easy to introduce bugs into your code.
Event-driven Execution
In most traditional imperative programming languages, a program begins and ends in a single function (e.g., main()
). If you pause the application at a debugger breakpoint, you can see a stack trace leading back to this main entrypoint.
JavaScript programs are not like this; instead, they are entirely event-driven. When a JavaScript library loads on a webpage, it subscribes to events it is interested in via event handlers. The browser triggers the relevant handler(s) when an event happens.
Browser APIs expose functionality via events wherever possible: user input, network requests, timers, storage interfaces, etc.
For example, the following code subscribes to clicks on a button, and lets the user know if they clicked the button after 5 seconds:
In JavaScript, everything is event-driven. You cannot opt out. At the same time, JavaScript execution blocks the UI from updating or receiving user input. As a result, a long-running event can crash the browser tab.
For example, the following code will crash your browser tab. You cannot click the button on the page. The browser will ask you if you want to kill the tab:
Run this code (WARNING: crashes tab!)
Web developers need to consciously avoid this problem. If an application needs to process a large amount of data (or do something else that is CPU intensive), it needs to space processing across multiple events to keep the webpage responsive.
Events and Debugging
Events have many implications for debugging. If you pause the application at a debugger breakpoint, you will see a stack trace that leads back to the start of the current event, but you won’t see the set of events that preceded it.
In the following contrived example, the breakpoint in bar
will let you example a stack trace back to foo
, but will not say anything about baz
and its timer:
Try running events-debugging.html
in Chrome, and open developer tools before the error alert occurs. It will open the debugger at the debugger statement.
Events make it difficult to determine the origin of a bug. In this example, the “bug” occurs because baz
sets error
to true. In an actual application, bugs may manifest many events in the future from the source of the bug.
If the bug is hard to reproduce, a developer will spend much of their time reproducing the bug and testing hypotheses by setting breakpoints in various locations (or by liberally inserting console.log
statements in the code).
Race Conditions
JavaScript is single-threaded, but it is not without races. Previous work has discussed some of these races in-depth.
There are two primary sources of races in JavaScript: those caused by event scheduling, and those caused by browser components executing in parallel with JavaScript execution. The latter are more subtle, and are not discussed by existing work on web application race detection.
Event Scheduling
If you have ever programmed with asynchronous I/O, then you may be aware that you cannot rely on the order in which multiple outstanding I/O operations complete. Similarly, you cannot rely on a JavaScript program to execute events in the same order for every execution.
As an example, the following code will display the content of data
when the user clicks the button. However, data
isn’t initialized until the timer fires, so if the user clicks the button prematurely, the webpage will display the message “undefined”:
The above example is contrived to keep these code snippets short, but these types of races are a common source of bugs!
Event scheduling-related races can be hard to find in more complicated web applications, and may only trigger in exceptional circumstances (e.g., a server is under heavy load, delaying requests).
Browser Components
While JavaScript is single threaded, the web browser is not. Browsers attempt to parallelize what they can, and can update state that is visible to JavaScript in parallel with JavaScript execution. As a result, JavaScript code can race with other components in the browser.
To make the problem concrete, suppose that a web page uses CSS to make a div
element periodically change its color. Further suppose that JavaScript code wishes to query its color:
One might assume that if isBlue(div)
returns true the first time that it would also return true the second time. However, this is not true. The color_change
animation occurs in parallel with JavaScript, so backgroundColor
changes while the script executes!
In addition to animations, network events can happen in parallel with JavaScript execution. For example, the height
and width
properties on an image are 0 until the image loads, and an image can load while JavaScript executes.
Conclusion
After reading through this document and trying some of the code samples, I hope you understand some of the challenges involved in writing and debugging web applications. Keep these challenges in mind during my talk.
If any of the examples or explanations are unclear or if you have questions, feel free to contact me.
Thanks for reading!