Concurrency and error handling
Keep shared data correct when things overlap, and fail in ways you can recover from.
What you will learn
- 01Why concurrency matters
- 02Race conditions
- 03Locks
- 04Alternatives to locking
- 05Errors: expected, unexpected, and bugs
- 06Handling errors well
- 07Checklist
Why concurrency matters
Real systems do many things at once: two gates open together, a payment callback lands during a booking. When overlapping work touches the same data, correctness is no longer about each step but about how steps interleave.
Race conditions
A race condition is a bug whose outcome depends on timing. The classic form is check then act.
function reserve(slot: Slot, plate: string) {
if (slot.free) { // 1. both cars see free
slot.free = false; // 2. both cars take it
slot.plate = plate;
}
}
Two cars call this at the same instant, both pass the check before either writes, and one slot is sold twice. It works in every test and fails at 6pm on a busy day.
Swipe sideways to pan · drag a box to move it
Swipe sideways to pan · drag a box to move it
Locks
A lock (mutex) lets one thread at a time run a critical section, the code that touches shared data.
await mutex.runExclusive(() => {
if (slot.free) { slot.free = false; slot.plate = plate; }
});
Keep critical sections short, since everyone else waits. Locks cost speed and bring a new danger: deadlock, where A holds one lock and waits for B while B holds that one and waits for A. Prevent it by always taking locks in the same order, or by holding only one at a time.
Swipe sideways to pan · drag a box to move it
Alternatives to locking
- Immutability: data that never changes needs no lock.
- Atomic operations: a single indivisible compare and set, supported by hardware and databases.
- Message passing: one owner thread changes the data and others send it requests through a queue, so there is no sharing.
- Optimistic concurrency: read a version number, work, and write only if the version is unchanged, otherwise retry. Good when conflicts are rare.
Swipe sideways to pan · drag a box to move it
A database can do much of this for you. A conditional update such as "set taken where free" is atomic, and a transaction isolates overlapping work.
Errors: expected, unexpected, and bugs
Not all failures are equal. Expected failures belong to the domain: payment declined, no free slot. Model them in the return type so callers must handle them. Unexpected failures come from the environment: a network drop, a full disk. A bug is the program being wrong, and it should fail loudly instead of continuing in a corrupt state.
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
Handling errors well
- Fail fast: check preconditions at the door and stop on the first violation.
- Never swallow: an empty
catchhides the problem and moves it somewhere harder to find. Handle it, or let it rise. - Add context: say what you were doing and with which id.
- Clean up: release locks and close resources even on failure, using
finallyor the language's equivalent. - Retry carefully: retry only temporary failures, wait longer between attempts (backoff), cap the attempts, and make the operation idempotent, meaning running it twice has the same effect as once. Charging a card twice because of a retry is the standard disaster.
Checklist
- Find the shared mutable data. Who can touch it at the same time?
- Prefer removing sharing over protecting it.
- Keep locks small and ordered.
- Decide for each error whether to recover, retry, or stop.