How do I fix a deadlock in async code?
Common causes in async code
In async code, deadlocks often happen when two or more tasks each hold a resource and wait for another resource held by the other. For example, Task A holds lock 1 and awaits lock 2, while Task B holds lock 2 and awaits lock 1. Neither can proceed.
Another frequent cause is mixing synchronous blocking calls (like fs.readFileSync or a busy loop) inside an async function that holds a lock. This blocks the event loop, preventing other tasks from releasing the lock they hold.
- Acquiring multiple locks in different orders
- Awaiting a Promise that never resolves while holding a lock
- Using a mutex that is not released on error paths
- Blocking the event loop with synchronous I/O
- Recursive locking without reentrancy support
How to prevent and fix
The most reliable fix is to enforce a global lock ordering: always acquire locks in the same sequence across your codebase. If that is not possible, use a lock with a timeout so a task can give up and retry, or use a single lock for related resources.
Also ensure locks are released in a finally block so errors do not leave them held. Avoid holding a lock across an await if you can restructure the code to do the critical work synchronously, or use a queue to serialize access.
- Define a strict order for acquiring locks and follow it everywhere
- Use lock timeouts and retry with backoff
- Release locks in a finally block
- Minimize the critical section; do not await inside it if possible
- Use a single lock or a queue for related operations
- Consider higher-level abstractions like a database transaction
Common mistakes
- Assuming that async/await prevents deadlocks; it can still occur when multiple locks are involved.
- Forgetting to release a lock when an error is thrown, causing other tasks to wait forever.
- Using a mutex that is not reentrant and then trying to lock it again in the same task.
