Asynchrony has become an inherent element of JavaScript, as an effort to improve the scalability and performance of modern web applications. To this end, JavaScript provides programmers with a wide range of constructs and features for developing code that performs asynchronous computations, including but not limited to timers, promises, and non-blocking I/O. However, the data flow imposed by asynchrony is implicit, and not always well-understood by the developers who introduce many asynchrony-related bugs to their programs. Worse, there are few tools and techniques available for analyzing and reasoning about such asynchronous applications. In this work, we address this issue by designing and implementing one of the first static analysis schemes capable of dealing with almost all the asynchronous primitives of JavaScript up to the 7th edition of the ECMAScript specification. Specifically, we introduce the callback graph, a representation for capturing data flow between asynchronous code. We exploit the callback graph for designing a more precise analysis that respects the execution order between different asynchronous functions. We parameterize our analysis with one novel context-sensitivity flavor, and we end up with multiple analysis variations for building callback graph. We performed a number of experiments on a set of hand-written and real-world JavaScript programs. Our results show that our analysis can be applied to medium-sized programs achieving 79% precision on average. The findings further suggest that analysis sensitivity is beneficial for the vast majority of the benchmarks. Specifically, it is able to improve precision by up to 28.5%, while it achieves an 88% precision on average without highly sacrificing performance. Keywords and phrases static analysis, asynchrony, JavaScript JavaScript is an integral part of web development. Since its initial release in 1995, it has evolved from a simple scripting language-primarily used for interacting with web pages-into a complex and general-purpose programming language used for developing both client-and server-side applications. The emergence of Web 2.0 along with the dynamic features of JavaScript, which facilitate a flexible and rapid development, have led to a dramatic increase in its popularity. Indeed, according to the annual statistics provided by Github, which is the leading platform for hosting open-source software, JavaScript is by far the most popular and active programming language from 2014 to 2018 [13] . Although the dominance of JavaScript is impressive, the community has widely criticized it because it poses many concerns as to the security or correctness of the programs [35] . JavaScript is a language with a lot of dynamic and metaprogramming features, including but arXiv:1901.03575v1 [cs.PL] 11 Jan 2019 XX:2 Static Analysis for Asynchronous JavaScript Programs not limited to prototype-based inheritance, dynamic property lookups, implicit type coercions, dynamic code loading, etc. Many developers often do not understand or do not properly use these features, introducing errors to their programs-which are difficult to debug-or baleful security vulnerabilities. In this context, JavaScript has attracted many engineers and researchers over the past decade to 1) study and reason about its peculiar characteristics, and 2) develop new tools and techniques-such as type analyzers [18, 22, 20] , IDE and refactoring tools [5, 6, 7, 10] , or bug and vulnerability detectors [28, 14, 33, 30, 4, 36] -to assist developers with the development and maintenance of their applications. Program analysis, and especially static analysis, which automatically computes facts about program's behavior without actually running it, plays a crucial role in the design of such tools [37] . Additionally, preserving the scalability of modern web applications has become more critical than ever. As an effort to improve the throughput of web programs, JavaScript has started to adopt an event-driven programming paradigm [3] . In this context, a code is executed asynchronously in response to certain events, e.g., user input, a response from a server, data read from disk, etc. In the first years of JavaScript, someone could come across that asynchrony mainly in a browser environment e.g., DOM events, AJAX calls, timers, etc. However, in recent years, asynchrony has become a salient and intrinsic element of the language, as newer versions of the language's core specification (i.e., ECMAScript) have introduced more and more asynchrony-related features. For example, ECMAScript 6 introduces promises; an essential element of asynchronous programming that allows developers to track the state of an asynchronous computation easily. Specifically, the state of a promise object can be one of: fulfilled: the associated operation is complete, and the promise object tracks its resulting value. rejected: the associated operation failed, and the promise object tracks its erroneous value. pending: the associated operation has been neither completed nor failed. Promises are particularly useful for asynchronous programming because they provide an intuitive way for creating chains of asynchronous computation, facilitating the enforcement of execution order as well as error propagation [11, 10] . To do that, promises trigger the execution of certain functions (i.e., callbacks) depending on their state, e.g., callbacks that are executed once a promise is fulfilled or rejected. For that reason, the API of promises provides the method x.then(f1, f2) for registering new callbacks (i.e., f1 and f2) on a promise object x. For example, we call the callback f1 when the promise is fulfilled, while we trigger the callback f2 once the promise is rejected. The method x.then() returns a new promise which the return value of the provided callbacks (i.e., f1, f2) fulfills. Since their initial introduction to the language, JavaScript developers have widely embraced promises; a study in 2015 showed that 75% of JavaScript frameworks use promises [11] . Building upon promises, newer versions of ECMAScript have added new language features related to asynchrony. Specifically, in ECMAScript 8, we have the async/await keywords. The async keyword declares a function as asynchronous which returns a promise fulfilled with its return value, while await x defers the execution of the asynchronous function in which is placed, until the promise object x is settled (i.e., it is either fulfilled or rejected). The latest edition of ECMAScript (ECMAScript 9) adds asynchronous iterators and generators that allow developers to iterate over asynchronous data sources. Beyond promises, many JavaScript applications are written to perform non-blocking I/O operations. Unlike traditional statements, when we perform a non-blocking I/O operation, the execution is not interrupted until that operation terminates. For instance, a file system operation is done asynchronously, which means that the execution proceeds to the next tasks while I/O takes place. Programmers often mix asynchronous I/O with promises. For instance, consider the real-world example of Figure 1 . At line 1, the code performs an asynchronous request and returns a promise object which is fulfilled asynchronously once the request succeeds. Then that promise object can be used for processing the response of the server asynchronously. For instance, at lines 2-23, we create a promise chain. The first callback of this chain (lines 2-14) clones the response of the request, and assigns it to the property response of the object honoka (line 3). Then, it parses the body of the response according to its type and fulfills the promise object allocated by the first invocation of then(). The second callback (lines 15-23) retrieves the headers of the response-which the statement at line 3 assigns to honoka.response-and if the content type is "application/json", it converts the data of the response into a JSON object (lines 17-19). Like the other characteristics of JavaScript, programmers do not always clearly understand asynchrony, as a large number of asynchrony-related questions issued in popular sites like stackoverflow.com 1 [26, 25] , or the number of bugs reported in open-source repositories [38, 4] indicate. However, existing tools and techniques have limited (and in many cases no) support for asynchronous programs. In particular, existing tools mainly focus on the event system of client-side JavaScript applications [17, 32] , and they lack the support of the more recent features added to the language like promises. Also, many previous works conservatively considered that all asynchronous callbacks processed by the event loop-the program point which continuously waits for new events to come and is responsible for the scheduling and execution of callbacks-can be called in any order [17, 32, 20] . However, such an approach may lead to imprecision and false positives. Back to the example of Figure 1 , it is easy to see that an analysis, which does not respect the execution order between the first and the 1 https://stackoverflow.com/ second callback, will report a type error at line 17 (access of honoka.response.headers.get (" Content -Type ")). Specifically, an imprecise analysis assumes that the callback defined at lines 15-23 might be executed first; therefore, honoka.response, assigned at line 3, might be uninitialized. In this work, we tackle those issues, by designing and implementing a static analysis that deals with asynchronous JavaScript programs. For that purpose, we first define a model for understanding and expressing a wide range of JavaScript's asynchronous constructs, and then we design a static analysis based on that. We propose a new representation, which we call callback graph, which provides information about the execution order of the asynchronous code. The callback graph proposed in this work tries to shed light on how data flow between asynchronous code is propagated. Contrary to previous works, we leverage the callback graph and devise a more precise analysis which respects the execution order of asynchronous functions. Furthermore, we parameterize our analysis with one novel context-sensitivity strategy designed for asynchronous code. Specifically, we distinguish data flow between asynchronous callbacks based on the promise object on which they have registered or the next computation to which execution proceeds. We propose a calculus, i.e., λ q , for modeling the asynchronous features in the JavaScript language, including timers, promises, and asynchronous I/O operations. Our calculus is a variation of existing calculi [24, 25] , and provides constructs and domains specifically targeted for our analysis ( §2). We design and implement a static analysis that is capable of handling asynchronous JavaScript programs by exploiting the abstract version of λ q . To the best of our knowledge, our analysis is the first to deal with JavaScript promises ( §3.1). We propose the callback graph, a representation which illustrates the execution order between asynchronous functions. Building on that, we propose a more precise analysis, (i.e., callback-sensitive analysis) which internally consults the callback graph to retrieve information about the temporal relations of asynchronous functions so that it propagates data flow accordingly. Besides that, we parameterize our analysis with a novel contextsensitivity flavor (i.e., QR-sensitivity) used for distinguishing asynchronous callbacks. ( §3.2, §3.3). We evaluate the performance and precision of our analysis on a set of micro benchmarks and a set of real-world JavaScript modules. For the impatient reader, we find that our prototype is able to analyze medium-sized asynchronous programs, and the analysis sensitivity is beneficial for improving the analysis precision. The results showed that our analysis is able to achieve a 79% precision for the callback graph, on average. The analysis sensitivity (i.e. callback-and QR-sensitivity) can further improve callback graph precision by up to 28.5% and reduce the total number of type errors by 16,7% as observed in the real-world benchmarks ( §4). As a starting point, we need to define a model to express asynchrony. The goal of this model is to provide us with the foundations for gaining a better understanding of the asynchronous primitives and ease the design of a static analysis for asynchronous JavaScript programs. This model is expressed through a calculus called λ q ; an extension of λ js which is the core calculus for JavaScript developed by [15] . The λ q calculus is designed to be flexible so that it can model various sources of asynchrony found in the language up to the 7th edition  The key component of our model is queue objects. Queue objects are closely related to JavaScript promises. Specifically, a queue object-like a promise-tracks the state of an asynchronous job, and it can be in one of the following states: 1) pending, 2) fulfilled or 3) rejected. A queue object may trigger the execution of callbacks depending on its state. Initially, a queue object is pending. A pending queue object can transition to a fulfilled or a rejected queue object. A queue object might be fulfilled or rejected with a provided value which is later passed as an argument in the execution of its callbacks. Once a queue object is either fulfilled or rejected, its state is final and cannot be changed. We keep the same terminology as promises, so if a queue object is either fulfilled or rejected, we call it settled. Figure 2 illustrates the syntax of λ q . For brevity, we present only the new constructs added to the language. Specifically, we add eight new expressions: This expression creates a fresh pending queue object with no callbacks associated with it. e 1 .fulfill(e 2 ): This expression fulfills the receiver (i.e., the expression e 1 ) with the value of e 2 . e 1 .reject(e 2 ): This expression rejects the receiver (i.e., the expression e 1 ) with the value of e 2 . e 1 .registerFul(e 2 , e 3 , . . . ): This expression registers the callback e 2 to the receiver. This callback is executed only when the receiver is fulfilled. This expression also expects another queue object passed as the second argument, i.e., e 3 . This queue object will be fulfilled with the return value of the callback e 2 . This allows us to model chains of promises where a promise resolves with the return value of another promise's callback. Static Analysis for Asynchronous JavaScript Programs This expression can also receive optional parameters (i.e., expressed through ". . . ") with which e 2 is called if the queue object is fulfilled with ⊥ value. We will justify later the intuition behind that. e 1 .registerRej(e 2 , e 3 , . . . ): The same as e.registerFul(. . . ) but this time the given callback is executed once the receiver is rejected. append(e): This expression appends the queue object e to the top of the current queue chain. As we shall see later, the top element of a queue chain corresponds to the queue object that is needed to be rejected if the execution encounters an uncaught exception. Observe that we use evaluation contexts [8, 15, 26, 24, 25 ] to express how the evaluation of an expression proceeds. The symbol E denotes which sub-expression is currently being evaluated. For instance, E.fulfill(e) describes that we evaluate the receiver of fulfill, whereas v.fulfill(E) implies that the receiver has been evaluated to a value v, and the evaluation now lies on the argument of fulfill. Beyond those expressions, the λ q calculus introduces a new value, that is, ⊥. This value differs from null and undefined because it expresses the absence of value and it does not correspond to any JavaScript value. Figure 3 presents the domains introduced in the semantics of λ q . In particular, a queue is a partial map of addresses to queue objects. The symbol l i -where i is a positive integerindicates an address. Notice that the set of the addresses also includes two special reserved addresses, i.e., l time , l io . We use these two addresses to store the queue objects responsible for keeping the state of callbacks related to timers and asynchronous I/O respectively (Section 2.2 explains how we model those JavaScript features). A queue object is described by its staterecall that a queue object is either pending or fulfilled and rejected with a value-a sequence of callbacks executed on fulfillment, and a sequence of callbacks called on rejection. The last element of a queue object is an address which corresponds to another queue object which is dependent on the current, i.e., it is settled whenever the current queue object is settled, and with the same state. We create such dependencies when we settle a queue object with another queue object. In this case, the receiver is dependent on the queue object used as an argument. Moving to the domains of callbacks, we see that a callback consists of an address, a function, and a list of values (i.e., arguments of the function). Note that the first component denotes the address of the queue object which the return value of the function is going to fulfill. In the list of callbacks κ ∈ ScheduledCallbacks, we keep the order in which callbacks are scheduled. Note that we maintain one more list of callbacks (i.e., τ ∈ ScheduledT imerIO) where we store the callbacks registered on the queue objects located at the addresses l time , l io . We defer the discussion about why we keep two separate lists until Section 2.1.3. A queue chain φ ∈ QueueChain is a sequence of addresses. In a queue chain, we store the queue object that we reject, if there is an uncaught exception in the current execution. Specifically, when we encounter an uncaught exception, we inspect the top element of the queue chain, and we reject it. If the queue chain is empty, we propagate the exception to the call stack as usual. Equipped with the appropriate definitions of the syntax and domains, in Figure 4 , we present the small-step semantics of λ q which is an adaptation of [24, 25] . Note that we demonstrate the most representative rules of our semantics; we omit some rules for brevity. For what follows, the binary operation denoted by the symbol · means the addition of an element to a list, the operation indicated by :: stands for list concatenation, while ↓ i means the projection of the i th element. The rules of our semantics adopt the following form: That form expresses that a given queue π, a queue chain φ, two sequences of callbacks κ and τ , and an expression e in the evaluation context E lead to a new queue π , a new queue chain φ , two new sequences of callbacks κ and τ , and a new expression e in the same evaluation context E, assuming that the expression e is reduced to e (i.e., e → e ). The [e-context] rule describes this behavior. The [newQ] rule creates a new queue object and adds it to the queue using a fresh address. This new queue object is pending, and it does not have any callbacks related to it. The [fulfill-pending] rule demonstrates the case when we fulfill a pending queue object with the value v, where v = ⊥ and does not correspond to any queue object. In particular, we first change the state of the receiver object from "pending" to "fulfilled". In turn, we update the already registered callbacks (if any) by setting the value v as the only argument of them. Then, we add the updated callbacks to the list of scheduled callbacks κ (assuming that the receiver is neither l time nor l io ). Also, observe that the initial expression is reduced to l.fulfill (v), that is, if there is a dependent queue object l, we also fulfill that queue object with the same value v. The [fulfill-pend-pend] describes the scenario of fulfilling a pending queue object p with another pending queue object v. In this case, we update the queue π by making the queue object p to be dependent on v. This means that we settle p whenever we settle v and in the same way. Notice that both p and v remain pending. The [fulfill-pend-ful] rule demonstrates the case when we try to fulfill a pending queue object p with the fulfilled queue object v. Then, p resolves with the value with which v is fulfilled. This is expressed by the resulting expression p.fulfill (v ). The [fulfill-pending-⊥] rule captures the case when we fulfill a queue object with a ⊥ value. This rule is the same with the [fulfill-pending] rule, however, this time we do not update the arguments of any callbacks registered on the queue object p. The [fulfill-settled] rule illustrates the case when we try to fulfill a settled queue object. Notice that this rule neither affects the queue π nor the lists of scheduled callbacks κ and τ . The [registerFul-pending] rule adds the provided callback f to the list of callbacks that we should execute once the queue object p is fulfilled. Note that this rule also associates , φ, κ, τ, E[e] → π , φ , κ , τ , E[e ] [e-context] φ, κ, τ, E[α] [newQ] φ, κ , τ, E[l.fulfill(v)] [fulfill-pending] φ, κ, τ, E[undef] [fulfill-pend-pend] φ, κ, τ, E[p.registerFul(f, p , r, n1, n2, . . . , nn) φ, κ, τ, E[p.registerFul(f, p , r, n1, n2, . . . , nn) φ, κ , τ, E[undef] [registerFul-fulfilled] φ, κ, τ, E[p.registerFul(f, p , r, n1, n2, . . . , nn) φ, κ , τ, E[undef] [registerFul-fulfilled-⊥] φ, κ, τ, E[p.registerFul(f, p , r, n1, n2, . . . , nn) φ , κ, τ, E[undef] [append] φ, κ, τ, E[undef] [pop] φ , κ, τ, E[p.reject(v)] [error] Figure 4 The semantics of λq XX:9 Figure 5 The semantics of the event loop. this callback with the queue object p given as the second argument. That queue object p is fulfilled upon the termination of f . Also, this rule adds any extra arguments passed in registerFul as arguments of f . The [registerFul-fulfilled] rule adds the given callback f to the list κ (assuming that the receiver is neither l time nor l io ). We use the fulfilled value of the receiver as the only argument of the given function. Like the previous rule, it relates the provided queue object p with the execution of the callback. This time we do ignore any extra arguments passed in registerFul, as we fulfill the queue object p with a value that is not ⊥. The [registerFul-fulfilled-⊥] rule describes the case where we register a callback f on a queue object fulfilled with a ⊥ value. Unlike the [registerFul-fulfilled] rule, this rule does not neglect any extra arguments passed in registerFul. In particular, it sets those arguments as parameters of the given callback. This distinction allows us to pass arguments explicitly to a callback. Most notably, these arguments are not dependent on the value with which a queue object is fulfilled or rejected. The [registerFul-timer-io-⊥] rule is the same as the previous one, but this time we deal with queue objects located either at l time or at l io . Thus, we add the given callback f to the list τ instead of κ. The [append] rule appends the element p to the front of the current queue chain. Note that this rule requires the element p to be a queue object (i.e., p ∈ dom(π)). On the other hand, the [pop] rule removes the top element of the queue chain. The [error] rule demonstrates the case when we encounter an uncaught exception, and the queue chain is not empty. In that case, we do not propagate the exception to the caller, but we pop the queue chain and get the top element. In turn, we reject the queue object p specified in that top element. In this way, we capture the actual behavior of the uncaught exceptions triggered during the execution of an asynchronous callback. A reader might wonder why do we keep two separate lists, i.e., the list τ for holding callbacks coming from the l time or l io queue objects, and the list κ for callbacks of any other queue objects. The intuition behind this design choice is that it is convenient for us to model the concrete semantics of the event loop correctly. In particular, the implementation of the event loop assigns different priorities to the callbacks depending on their kind [24, 31] . For example, the event loop processes a callback of a promise object before any callback of a timer or an asynchronous I/O operation regardless of their registration order. In this context, Figure 5 demonstrates the semantics of the event loop. The [event-loop] rule pops the first scheduled callback from the list κ. Then, we get the queue object specified in that callback, and we attach it to the front of the queue chain. Adding the queue object q to the top of the queue chain allows us to reject that queue object if there is an uncaught Figure 6 Extending the syntax of λq to deal with timers and asynchronous I/O. exception during the execution of f . In this case, the evaluation of fulfill will not have any effect on the already rejected queue object q (recall the [fullfill-settled] rule). Also, observe how the event loop is reduced, i.e., q.fulfill(f (a)); pop(); •. Specifically, once we execute the callback f and fulfill the dependent queue object q with the return value of that callback, we evaluate pop(), that is, we pop the top element of the queue chain before we re-evaluate the event loop. This is an invariant of the semantics of the event loop: every time we evaluate it, the queue chain is always empty. The [event-loop-timers-io] rule handles the case when the list κ is empty. In other words, that rule states that if there are not any callbacks, which neither come from the l time nor the l io queue object, inspect the list τ , and pick non-deterministically one of those. Selecting a callback non-deterministically allows us to over-approximate the actual behavior of the event loop regarding its different execution phases [24] . Overall, that rule describes the scheduling policy presented in the work of [24] , where initially we look for any callbacks of promises, and if there exist, we select one of those. Otherwise, we choose any callback associated with timers and asynchronous I/O at random. φ, κ, q.registerFul(f, q, n1, . . . ) [add-timer-callback] φ, κ, τ, addIOCallback(f, n1, . . . ) → π, φ, κ, q.registerFul(f, q, n1, . . . ) [add-io-callback] To model timers and asynchronous I/O, we follow a similar approach to the work of [24] . Specifically, we start with an initial queue π, which contains two queue objects: the q time , and q io which are located at the l time and l io respectively. Both q time and q io are initialized as ((fulfilled, ⊥), [], [], ⊥). Besides that, we extend the syntax of λ q by adding two more expressions. Figure 6 shows the extended syntax of λ q to deal with timers and asynchronous I/O, while Figure 7 presents the rules related to those expressions. The new expressions have high correspondence to each other. Specifically, the addTimerCallback(. . . ) construct adds the callback e 1 to the queue object located at the address l time . We call the provided callback The arguments of that callback are any optional parameters passed in addTimerCallback, i.e., e 2 , e 3 , and so on. From Figure 7 , we observe that the [add-timer-callback] rule retrieves the queue object q corresponding to the address l time . Recall again that the l time can be found in the initial queue. Then, the given expression is reduced to q.registerFul(f, q, n 1 , . . . ). In particular, we add a new callback f to the XX:11 queue object found at l time . Observe that we pass the same queue object (i.e., q) as the second argument of registerFul. That means that the execution of f does not affect any queue object since q is already settled. Recall that according to the [fulfill-settled] rule (Figure 4 ), trying to fulfill (and similarly to reject) a settled queue object does not have any effect. Beyond that, since q is fulfilled with ⊥, the extra arguments (i.e., n 1 ,. . . ) are also passed as arguments in the invocation of f . The semantics of the addIOCallback(. . . ) primitive is the same with that of addTimerCallback(. . . ); however, this time, we use the queue object located at l io . The queue objects and their operations introduced in λ q are very closely related to JavaScript promises. Therefore, the translation of promises' operations into λ q is straightforward. We model every property and method (except for Promise.all()) by faithfully following the ECMAScript specification. Figure 8 , we see how we model the Promise.resolve() function in terms of λ q . The JavaScript Promise.resolve() function creates a new promise, and resolves it with the given value. According to ECMAScript, if the given value is a thenable, (i.e., an object which has a property named "then" and that property is a callable), the created promise resolves asynchronously. Specifically, we execute the function value.then() asynchronously, and we pass the resolving functions (i.e., fulfill, reject) as its arguments. Observe how the expressiveness of λ q can model this source of asynchrony (lines 4-6). First, we create a fresh queue object t, and we fulfill it with ⊥ (lines 4, 5). Then, at line 6, we schedule the execution of value.then() by registering it on the newly created queue object t. Notice that we also pass promise.fulfill and promise.reject as extra arguments. That means that those functions will be the actual arguments of value.then() because t is fulfilled with ⊥. On the other hand, if value is not a thenable, we synchronously resolve the created promise using the promise.fulfill construct at line 8. The Core Analysis The λ q calculus presented in Section 2 is the touchstone of the static analysis proposed for asynchronous JavaScript programs. The analysis is designed to be sound; thus, we devise abstract domains and semantics that over-approximate the behavior of λ q . Currently, there are few implementations available for asynchronous JavaScript, and previous efforts mainly focus on modeling the event system of client-side applications [17, 32] . To the best of our knowledge, it is the first static analysis for ES6 promises. The rest of this section describes the details of the analysis. l ∈ Addr = {l i | i is an allocation site} ∪ {l time , l io } π ∈ Queue = Addr → P( QueueObject) q ∈ QueueObject = QueueState × P( Callback) × P( Callback) × P( Addr) qs ∈ QueueState = {pending} ∪ ({fulfilled, rejected} × V alue) clb ∈ Callback = Addr × Addr × F × V alue * κ ∈ ScheduledCallbacks = (P( Callback)) * τ ∈ ScheduledT imerIO = (P( Callback)) * φ ∈ QueueChain = (P( Addr)) * The abstract domains of λq. Figure 9 presents the abstract domains of the λ q calculus that underpin our static analysis. Below we make a summary of our primary design choices. Abstract Addresses: As a starting point, we employ allocation site abstraction for modeling the space of addresses. It is the standard way used in literature for abstracting addresses which keeps the domain finite [18, 26] . Also note that we still define two internal addresses, i.e., l time , l io , corresponding to the addresses of the queue objects responsible for timers and asynchronous I/O respectively. We define an abstract queue as the partial map of abstract addresses to an element of the power set of abstract queue objects. Therefore, an address might point to multiple queue objects. This abstraction over-approximates the behavior of λ q and allows us to capture all possible program behaviors that might stem from the analysis imprecision.
Sotiropoulos et al. REF introduce λ q , a model of asynchrony in JavaScript, as well as the callback graph, which describes the possible orderings of callback execution.
Static Analysis for Asynchronous JavaScript Programs
ArXiv
ArXiv
Computer Science