Finding broken promises in asynchronous JavaScript programs

Saba Alimadadi, Di Zhong, Magnus Madsen, Frank Tip
2018 Proceedings of the ACM on Programming Languages (PACMPL)  
Recently, promises were added to ECMAScript 6, the JavaScript standard, in order to provide better support for the asynchrony that arises in user interfaces, network communication, and non-blocking I/O. Using promises, programmers can avoid common pitfalls of event-driven programming such as event races and the deeply nested counterintuitive control flow referred to as łcallback hellž. Unfortunately, promises have complex semantics and the intricate controlś and data-flow present in
more » ... d code hinders program comprehension and can easily lead to bugs. The promise graph was proposed as a graphical aid for understanding and debugging promise-based code. However, it did not cover all promise-related features in ECMAScript 6, and did not present or evaluate any technique for constructing the promise graphs. In this paper, we extend the notion of promise graphs to include all promise-related features in ECMAScript 6, including default reactions, exceptions, and the synchronization operations race and all. Furthermore, we report on the construction and evaluation of PromiseKeeper, which performs a dynamic analysis to create promise graphs and infer common promise anti-patterns. We evaluate PromiseKeeper by applying it to 12 open source promise-based Node.js applications. Our results suggest that the promise graphs constructed by PromiseKeeper can provide developers with valuable information about occurrences of common anti-patterns in their promise-based code, and that promise graphs can be constructed with acceptable run-time overhead. due to event race errors [Adamsen et al. 2017b,a; Petrov et al. 2012; Raychev et al. 2013; Zhang and Wang 2017; Zheng et al. 2011] , and lost events and dead listeners [Madsen et al. 2015] that are hard to debug. Promises aim to overcome these problems by providing an abstraction for the result of an asynchronous computation. A promise is in one of three states: pending, fulfilled, or rejected. A pending promise has not yet been settled, i.e., resolved or rejected with a value. A fulfilled promise holds the result of an asynchronous computation that has successfully completed, whereas a rejected promise holds an error value of an asynchronous computation that somehow failed. Once a promise has been fulfilled or rejected, its value cannot change, i.e. a promise can only be settled once. Each promise object is equipped with two functions, resolve and reject, that are used to to fulfill or reject the promise. Promises enable programmers to compose asynchronous computations by associating reactions with a promise. When a promise is resolved or rejected, the corresponding reaction is executed and the resulting value is wrapped in another promise, enabling programmers to create a chain of asynchronous computations where each computation depends on the value of the previous computation. Promises support proper error-handling by allowing errors to be propagated along these promise chains. Unfortunately, promises are complex in their own right and JavaScript programmers are easily confused by their semantics, causing them to make mistakes that result in hard-to-debug errors [Madsen et al. 2015] . For example, programmers may forget to resolve or reject a promise on all paths through the program, or forget to register a resolve and reject reaction on a promise. Other common mistakes are situations where a promise is unintentionally resolved with the value undefined when a function that was registered as a reaction returns implicitly, where an attempt is made to resolve or reject a promise that was already settled, or where programmers unintentionally construct a promise chain that has an fork. Prior work ] provided a formal semantics for a core subset of JavaScript promises, and proposed the promise graph as a visual aid for understanding and debugging promisebased programs. This work argued for the usability of promise graphs, but it did not handle all promise-related features in ECMAScript 6, and it did not present any technique for computing promise graphs. Moreover, its evaluation was limited to a case study in which promise graphs were constructed manually for small program fragments taken from the StackOverflow website. In this paper, we present an extension of the promise graph to handle all promise-related features of the ECMAScript 6 standard, including exceptions, default reactions, and the race and all constructs that are used for synchronization on multiple promises. We report on the implementation of a tool, PromiseKeeper, that automatically constructs promise graphs based on dynamic analysis. In an empirical evaluation, we apply PromiseKeeper to 12 promise-based Node.js applications taken from GitHub. Our findings show that PromiseKeeper is capable of constructing promise graphs for large and complex applications with acceptable run-time overhead. Furthermore, we show that PromiseKeeper is able to detect anti-patterns such as missing reject reactions, attempts to settle a promise multiple times, unsettled promises, unnecessary promises, implicit returns in reactions, and unreachable reactions that warrant further investigation by a developer. Such anti-patterns manifest questionable coding practices that are often, but not always correlated with bugs [Gamma 1995]. We convey these findings using a visual representation that enables developers to quickly obtain an understanding of the behavior of, and identify potential problems in promise-based code. In summary, this paper makes the following contributions:
doi:10.1145/3276532 fatcat:y77ogjqnyfgxhndyh4vvkll4cq