Summary
When a user rapidly toggles a task's completion status (or two browser tabs update the same task simultaneously), the PATCH /api/tasks/:id endpoint performs a read-then-write without any concurrency control. The last write wins, silently discarding the intermediate state update.
Root Cause
In backend/controllers/taskController.js, the update uses findByIdAndUpdate with $set, which is not atomic for conditional updates:
const task = await Task.findById(taskId);
task.status = newStatus;
task.updatedAt = Date.now();
await task.save();
Two concurrent requests can both read the same document state, then both write, with the second write overwriting the first.
Impact
Users lose task state changes silently. No error is shown; the UI appears to accept the change, but the server reverts it on the next fetch.
Expected Behavior
Status updates should be atomic. Using MongoDB's findOneAndUpdate with version-based optimistic locking or an atomic $set prevents lost updates.
Proposed Fix
Replace the read-then-write pattern with:
const updated = await Task.findOneAndUpdate(
{ _id: taskId, userId, __v: expectedVersion },
{ $set: { status: newStatus }, $inc: { __v: 1 } },
{ new: true, runValidators: true }
);
if (!updated) {
return res.status(409).json({ error: "Conflict: task was modified by another request" });
}
Enable optimisticConcurrency: true in the Mongoose schema options.
Summary
When a user rapidly toggles a task's completion status (or two browser tabs update the same task simultaneously), the
PATCH /api/tasks/:idendpoint performs a read-then-write without any concurrency control. The last write wins, silently discarding the intermediate state update.Root Cause
In
backend/controllers/taskController.js, the update usesfindByIdAndUpdatewith$set, which is not atomic for conditional updates:Two concurrent requests can both read the same document state, then both write, with the second write overwriting the first.
Impact
Users lose task state changes silently. No error is shown; the UI appears to accept the change, but the server reverts it on the next fetch.
Expected Behavior
Status updates should be atomic. Using MongoDB's
findOneAndUpdatewith version-based optimistic locking or an atomic$setprevents lost updates.Proposed Fix
Replace the read-then-write pattern with:
Enable
optimisticConcurrency: truein the Mongoose schema options.