Skip to main content
Autonomous AgentsAutonomous Agent89 lines

Concurrency Patterns

Understanding and implementing concurrent code with async/await, Promises, thread safety, race conditions, and strategies for debugging timing-dependent bugs.

Quick Summary18 lines
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.

## Key Points

- Use `async/await` for I/O-bound operations: network requests, file system access, database queries. It allows the runtime to do other work while waiting.
- Always `await` async function calls. Forgetting `await` creates a dangling Promise that runs independently — errors will be swallowed and ordering will be wrong.
- Use `try/catch` around `await` calls 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 `async` changes its return type.
- In loops, consider whether iterations should run sequentially (`for...of` with `await`) or concurrently (`Promise.all` with `map`).
- `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 use `try/catch` with `await`.
- Avoid mixing callback-style and Promise-style code. Convert callbacks to Promises with `util.promisify` (Node) or manual wrapping.
- 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.
skilldb get autonomous-agent-skills/Concurrency PatternsFull skill: 89 lines
Paste into your CLAUDE.md or agent config

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/await for I/O-bound operations: network requests, file system access, database queries. It allows the runtime to do other work while waiting.
  • Always await async function calls. Forgetting await creates a dangling Promise that runs independently — errors will be swallowed and ordering will be wrong.
  • Use try/catch around await calls 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 async changes its return type.
  • In loops, consider whether iterations should run sequentially (for...of with await) or concurrently (Promise.all with map).

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 use try/catch with await.
  • 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 multiprocessing for CPU parallelism, asyncio for 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 SharedArrayBuffer and Atomics only 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 -race flag) 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.

Install this skill directly: skilldb add autonomous-agent-skills

Get CLI access →