Concurrency Patterns
Understanding and implementing concurrent code with async/await, Promises, thread safety, race conditions, and strategies for debugging timing-dependent bugs.
Concurrency Patterns
You are an AI agent writing and debugging concurrent code. Your role is to implement correct concurrent patterns using async/await, Promises, threads, and message passing — while avoiding race conditions, deadlocks, and the subtle timing bugs that make concurrency difficult.
Philosophy
Concurrency bugs are the hardest class of bugs because they are non-deterministic. Code that works 99% of the time can fail in production under load, and the failure may be unreproducible in development. The defense against this is not cleverness but simplicity: use the simplest concurrency model that solves the problem, minimize shared mutable state, and prefer well-tested primitives over custom synchronization logic. When you must share state, make the access patterns obvious and explicit.
Techniques
Async/Await Patterns
- Use
async/awaitfor I/O-bound operations: network requests, file system access, database queries. It allows the runtime to do other work while waiting. - Always
awaitasync function calls. Forgettingawaitcreates a dangling Promise that runs independently — errors will be swallowed and ordering will be wrong. - Use
try/catcharoundawaitcalls for error handling. An unhandled rejection in an async function may crash the process or be silently ignored. - Async functions always return Promises. Even if the function body is synchronous, wrapping it in
asyncchanges its return type. - In loops, consider whether iterations should run sequentially (
for...ofwithawait) or concurrently (Promise.allwithmap).
Promise Handling
Promise.all()runs Promises concurrently and fails fast — if any Promise rejects, the whole result rejects. Use it when all results are needed and any failure is fatal.Promise.allSettled()runs Promises concurrently and waits for all to complete, regardless of success or failure. Use it when you need results from as many as possible.Promise.race()resolves with the first settled Promise. Use it for timeouts: race the operation against a timer.- Never create Promises without handling rejection. Attach
.catch()or usetry/catchwithawait. - Avoid mixing callback-style and Promise-style code. Convert callbacks to Promises with
util.promisify(Node) or manual wrapping.
Race Conditions
- A race condition occurs when the result depends on the timing of events that are not guaranteed to occur in a specific order.
- Check-then-act patterns are a classic source: checking if a file exists then writing to it — another process may create the file between the check and the write.
- In web applications, race conditions appear when multiple requests modify the same resource. Use optimistic locking (version numbers) or pessimistic locking (database locks).
- In frontend code, race conditions occur when a fast response arrives after a slow one — the UI shows stale data. Cancel or ignore outdated requests.
- Use atomic operations when available. Database transactions,
compareAndSet, and atomic file writes prevent intermediate states.
Deadlocks
- A deadlock occurs when two or more tasks wait for each other to release resources, and none can proceed.
- The classic pattern: Task A locks resource 1 and waits for resource 2. Task B locks resource 2 and waits for resource 1. Neither can proceed.
- Prevention: always acquire locks in a consistent global order. If every task locks resources in the same sequence, circular waits cannot form.
- Detection: if a system hangs without error, suspect a deadlock. Examine what each task is waiting for.
- Avoidance: prefer lock-free designs. Use message passing, immutable data, or single-writer patterns instead of multiple locks.
Thread Safety
- Data accessed by multiple threads must be protected. Unprotected concurrent reads and writes produce corrupted data.
- In JavaScript (single-threaded event loop), race conditions come from interleaved async operations, not threads. But Web Workers and Worker Threads introduce true parallelism.
- In Go, use channels for communication between goroutines. The motto is "share memory by communicating, do not communicate by sharing memory."
- In Python, the Global Interpreter Lock (GIL) prevents true parallelism for CPU-bound code. Use
multiprocessingfor CPU parallelism,asynciofor I/O parallelism. - In Rust, the borrow checker prevents data races at compile time. If it compiles, shared mutable access is safe.
- Use thread-safe data structures (
ConcurrentHashMap,sync.Map,Mutex<T>) when sharing state is necessary.
Worker Threads and Message Passing
- Offload CPU-intensive work to worker threads to keep the main thread responsive.
- Workers communicate via message passing (postMessage/onmessage in browsers, parentPort in Node workers). Data is copied or transferred, not shared.
- Use
SharedArrayBufferandAtomicsonly when message passing overhead is unacceptable and you understand the memory model. - In Go, goroutines with channels provide lightweight concurrency with clean message passing semantics.
- Size worker pools appropriately: too few waste CPU, too many waste memory and increase context switching.
Choosing Parallelism Strategies
- I/O-bound work (API calls, database queries, file reads): use async/await. One thread can handle many I/O operations concurrently.
- CPU-bound work (image processing, data transformation, compression): use worker threads or separate processes. Async does not help because the CPU is the bottleneck.
- Mixed workloads: use async for I/O and offload CPU work to workers.
- Consider whether operations need to be concurrent at all. Sequential code is easier to reason about and debug. Add concurrency only when performance requires it.
Debugging Timing-Dependent Bugs
- Timing bugs are hard to reproduce because they depend on execution order. Adding logging can change timing and hide the bug.
- Use deterministic tests: mock timers, control scheduling, use barriers to force specific execution orders.
- Look for shared mutable state accessed without synchronization. This is the root cause of most concurrency bugs.
- Stress testing (running many concurrent operations) can surface race conditions that appear rarely under normal load.
- Thread sanitizers (TSan) and race detectors (Go's
-raceflag) can detect data races automatically.
Best Practices
- Default to sequential code. Add concurrency only when there is a measured performance need.
- Use structured concurrency: every concurrent task should have a clear owner that waits for its completion and handles its errors.
- Cancel outdated work. When a new request supersedes an old one, cancel the old one to avoid wasted work and stale results.
- Set timeouts on all concurrent operations. Without timeouts, a hung operation can block the system indefinitely.
- Test concurrent code with many iterations and concurrent executions, not just single-threaded happy paths.
Anti-Patterns
- Fire and forget: Starting async operations without awaiting or handling errors. Failures become silent, ordering becomes unpredictable.
- Shared mutable state without protection: The root of nearly all concurrency bugs. Protect shared state with locks, atomic operations, or immutable data.
- Blanket parallelization: Making everything concurrent without measuring adds complexity without proportional benefit. Profile first.
- Nested locks: Acquiring locks inside other locks invites deadlocks. Keep lock scopes narrow and avoid nesting.
- Busy waiting: Spinning in a loop checking a condition wastes CPU. Use event-based signaling (condition variables, Promises, channels).
- Ignoring cancellation: Long-running concurrent tasks that cannot be cancelled waste resources and block shutdown.
- Assuming ordering without synchronization: Two async operations started in sequence may complete in any order. Use explicit synchronization when ordering matters.
Related Skills
Abstraction Control
Avoiding over-abstraction and unnecessary complexity by choosing the simplest solution that solves the actual problem
Accessibility Implementation
Making web content accessible through ARIA attributes, semantic HTML, keyboard navigation, screen reader support, color contrast, focus management, and WCAG compliance.
API Design Patterns
Designing and implementing clean APIs with proper REST conventions, pagination, versioning, authentication, and backward compatibility.
API Integration
Integrating with external APIs effectively — reading API docs, authentication patterns, error handling, rate limiting, retry with backoff, response validation, SDK vs raw HTTP decisions, and API versioning.
Assumption Validation
Detecting and validating assumptions before acting on them to prevent cascading errors from wrong guesses
Authentication Implementation
Implementing authentication flows correctly including OAuth 2.0/OIDC, JWT handling, session management, password hashing, MFA, token refresh, and CSRF protection.