A silent data-loss bug, and the one-line fix that ended it
For a few weeks, Lucy Tutor — the IELTS writing tutor I built for myself — had a bug that only showed up sometimes: a completed drill day would just be missing the next time I loaded the app. No error, no warning. The data was there when I graded the essay, gone by the time I refreshed.
The state lives in a JSON file on GitHub, and every write goes through the GitHub
Contents API, which requires the current file SHA to accept an update. I was firing
several writes in parallel with Promise.all — drill history, error log, collocation
bank — because parallel felt faster and there was no obvious reason not to.
That was the bug. Two writes racing against the same file both read the same starting SHA, both tried to commit, and the second one either failed outright or silently overwrote the first with stale data. Nothing crashed. It just lost whichever write lost the race.
The fix was one word: await each write in sequence instead of Promise.all-ing
them. Slower, technically — a few hundred milliseconds instead of running concurrently
— and completely correct, because now every write reads the SHA the previous write
actually produced.
The lesson generalizes past this one bug: “parallel is faster” is only true when the operations don’t share mutable state. Once they do, parallel isn’t an optimization, it’s a race condition wearing a performance improvement as a disguise. Same instinct that applies to running ad platform pulls against a shared budget ledger — sequence matters more than speed the moment two writes can touch the same record.