]>
Commit | Line | Data |
---|---|---|
1 | # Disallow assignments that can lead to race conditions due to usage of `await` or `yield` (require-atomic-updates) | |
2 | ||
3 | When writing asynchronous code, it is possible to create subtle race condition bugs. Consider the following example: | |
4 | ||
5 | ```js | |
6 | let totalLength = 0; | |
7 | ||
8 | async function addLengthOfSinglePage(pageNum) { | |
9 | totalLength += await getPageLength(pageNum); | |
10 | } | |
11 | ||
12 | Promise.all([addLengthOfSinglePage(1), addLengthOfSinglePage(2)]).then(() => { | |
13 | console.log('The combined length of both pages is', totalLength); | |
14 | }); | |
15 | ``` | |
16 | ||
17 | This code looks like it will sum the results of calling `getPageLength(1)` and `getPageLength(2)`, but in reality the final value of `totalLength` will only be the length of one of the two pages. The bug is in the statement `totalLength += await getPageLength(pageNum);`. This statement first reads an initial value of `totalLength`, then calls `getPageLength(pageNum)` and waits for that Promise to fulfill. Finally, it sets the value of `totalLength` to the sum of `await getPageLength(pageNum)` and the *initial* value of `totalLength`. If the `totalLength` variable is updated in a separate function call during the time that the `getPageLength(pageNum)` Promise is pending, that update will be lost because the new value is overwritten without being read. | |
18 | ||
19 | One way to fix this issue would be to ensure that `totalLength` is read at the same time as it's updated, like this: | |
20 | ||
21 | ```js | |
22 | async function addLengthOfSinglePage(pageNum) { | |
23 | const lengthOfThisPage = await getPageLength(pageNum); | |
24 | ||
25 | totalLength += lengthOfThisPage; | |
26 | } | |
27 | ``` | |
28 | ||
29 | Another solution would be to avoid using a mutable variable reference at all: | |
30 | ||
31 | ```js | |
32 | Promise.all([getPageLength(1), getPageLength(2)]).then(pageLengths => { | |
33 | const totalLength = pageLengths.reduce((accumulator, length) => accumulator + length, 0); | |
34 | ||
35 | console.log('The combined length of both pages is', totalLength); | |
36 | }); | |
37 | ``` | |
38 | ||
39 | ## Rule Details | |
40 | ||
41 | This rule aims to report assignments to variables or properties in cases where the assignments may be based on outdated values. | |
42 | ||
43 | ### Variables | |
44 | ||
45 | This rule reports an assignment to a variable when it detects the following execution flow in a generator or async function: | |
46 | ||
47 | 1. The variable is read. | |
48 | 2. A `yield` or `await` pauses the function. | |
49 | 3. After the function is resumed, a value is assigned to the variable from step 1. | |
50 | ||
51 | The assignment in step 3 is reported because it may be incorrectly resolved because the value of the variable from step 1 may have changed between steps 2 and 3. In particular, if the variable can be accessed from other execution contexts (for example, if it is not a local variable and therefore other functions can change it), the value of the variable may have changed elsewhere while the function was paused in step 2. | |
52 | ||
53 | Note that the rule does not report the assignment in step 3 in any of the following cases: | |
54 | ||
55 | * If the variable is read again between steps 2 and 3. | |
56 | * If the variable cannot be accessed while the function is paused (for example, if it's a local variable). | |
57 | ||
58 | Examples of **incorrect** code for this rule: | |
59 | ||
60 | ```js | |
61 | /* eslint require-atomic-updates: error */ | |
62 | ||
63 | let result; | |
64 | ||
65 | async function foo() { | |
66 | result += await something; | |
67 | } | |
68 | ||
69 | async function bar() { | |
70 | result = result + await something; | |
71 | } | |
72 | ||
73 | async function baz() { | |
74 | result = result + doSomething(await somethingElse); | |
75 | } | |
76 | ||
77 | async function qux() { | |
78 | if (!result) { | |
79 | result = await initialize(); | |
80 | } | |
81 | } | |
82 | ||
83 | function* generator() { | |
84 | result += yield; | |
85 | } | |
86 | ``` | |
87 | ||
88 | Examples of **correct** code for this rule: | |
89 | ||
90 | ```js | |
91 | /* eslint require-atomic-updates: error */ | |
92 | ||
93 | let result; | |
94 | ||
95 | async function foobar() { | |
96 | result = await something + result; | |
97 | } | |
98 | ||
99 | async function baz() { | |
100 | const tmp = doSomething(await somethingElse); | |
101 | result += tmp; | |
102 | } | |
103 | ||
104 | async function qux() { | |
105 | if (!result) { | |
106 | const tmp = await initialize(); | |
107 | if (!result) { | |
108 | result = tmp; | |
109 | } | |
110 | } | |
111 | } | |
112 | ||
113 | async function quux() { | |
114 | let localVariable = 0; | |
115 | localVariable += await something; | |
116 | } | |
117 | ||
118 | function* generator() { | |
119 | result = (yield) + result; | |
120 | } | |
121 | ``` | |
122 | ||
123 | ### Properties | |
124 | ||
125 | This rule reports an assignment to a property through a variable when it detects the following execution flow in a generator or async function: | |
126 | ||
127 | 1. The variable or object property is read. | |
128 | 2. A `yield` or `await` pauses the function. | |
129 | 3. After the function is resumed, a value is assigned to a property. | |
130 | ||
131 | This logic is similar to the logic for variables, but stricter because the property in step 3 doesn't have to be the same as the property in step 1. It is assumed that the flow depends on the state of the object as a whole. | |
132 | ||
133 | Example of **incorrect** code for this rule: | |
134 | ||
135 | ```js | |
136 | /* eslint require-atomic-updates: error */ | |
137 | ||
138 | async function foo(obj) { | |
139 | if (!obj.done) { | |
140 | obj.something = await getSomething(); | |
141 | } | |
142 | } | |
143 | ``` | |
144 | ||
145 | Example of **correct** code for this rule: | |
146 | ||
147 | ```js | |
148 | /* eslint require-atomic-updates: error */ | |
149 | ||
150 | async function foo(obj) { | |
151 | if (!obj.done) { | |
152 | const tmp = await getSomething(); | |
153 | if (!obj.done) { | |
154 | obj.something = tmp; | |
155 | } | |
156 | } | |
157 | } | |
158 | ``` | |
159 | ||
160 | ## Options | |
161 | ||
162 | This rule has an object option: | |
163 | ||
164 | * `"allowProperties"`: When set to `true`, the rule does not report assignments to properties. Default is `false`. | |
165 | ||
166 | ### allowProperties | |
167 | ||
168 | Example of **correct** code for this rule with the `{ "allowProperties": true }` option: | |
169 | ||
170 | ```js | |
171 | /* eslint require-atomic-updates: ["error", { "allowProperties": true }] */ | |
172 | ||
173 | async function foo(obj) { | |
174 | if (!obj.done) { | |
175 | obj.something = await getSomething(); | |
176 | } | |
177 | } | |
178 | ``` | |
179 | ||
180 | ## When Not To Use It | |
181 | ||
182 | If you don't use async or generator functions, you don't need to enable this rule. |