Read-Modify-Write Across await
ID |
javascript.require_atomic_updates |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
JavaScript |
Tags |
async, race, reliability |
Description
Reports the read-modify-write pattern x = (await f(x)) … where the same binding is read on the right-hand side, an await suspends the function, and then the resulting value is assigned back to x. Between the read and the write any other call site can also touch x, so the write may clobber a later update — the textbook async race condition.
v1 is conservative: only flags the structural shape x = await …x…; where the LHS is a simple identifier and some descendant of the RHS reads that same identifier.
Rationale
// Bad — `counter` is read at scheduling time, then re-written after the
// await. Concurrent calls overwrite each other.
async function increment() {
let counter = 0;
counter = (await load()) + counter;
return counter;
}
// Bad — same hazard with property reads on the LHS binding.
async function totaler() {
let total = 0;
total = (await fetchPart()).value + total;
return total;
}
Remediation
Either read the value into a local before the await and assign once at the end, or use a lock / atomic primitive when concurrent access is the actual requirement:
// Good — capture the await result, then write once.
async function increment() {
let counter = 0;
const delta = await load();
counter = counter + delta;
return counter;
}
For genuinely concurrent state, consider a queue, a mutex helper, or a single-writer pattern where one task owns the binding.