JavaScript
async programming
concurrency
locks
web development

Javascript, async, lock?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

JavaScript does not have a built-in mutex like many multithreaded languages, but async race conditions still happen whenever multiple tasks await in the middle of a shared critical section. In those cases, an async lock or mutex is just a queue that guarantees one piece of asynchronous work enters the protected section at a time.

Core Sections

Why a lock can matter in single-threaded JavaScript

JavaScript usually runs one piece of synchronous code at a time, but asynchronous operations can interleave in surprising ways. If two requests both read shared state, await, and then write back, the later completion can overwrite the earlier result.

javascript
1let counter = 0;
2
3async function incrementSlowly() {
4  const snapshot = counter;
5  await new Promise(resolve => setTimeout(resolve, 10));
6  counter = snapshot + 1;
7}

If incrementSlowly() runs twice concurrently, the final value may still be 1 instead of 2. That is the kind of bug an async lock prevents.

A minimal async mutex

One common approach is to chain promises so each caller waits for the previous one to finish.

javascript
1class Mutex {
2  constructor() {
3    this.queue = Promise.resolve();
4  }
5
6  async runExclusive(task) {
7    const previous = this.queue;
8    let release;
9    this.queue = new Promise(resolve => {
10      release = resolve;
11    });
12
13    await previous;
14
15    try {
16      return await task();
17    } finally {
18      release();
19    }
20  }
21}
22
23const mutex = new Mutex();
24let counter = 0;
25
26async function safeIncrement() {
27  await mutex.runExclusive(async () => {
28    const snapshot = counter;
29    await new Promise(resolve => setTimeout(resolve, 10));
30    counter = snapshot + 1;
31  });
32}

Now concurrent calls are serialized through the mutex queue.

What should go inside the lock

Only place the truly shared critical section inside the lock. If you lock too much, unrelated work is forced to wait and throughput drops. If you lock too little, the race remains.

A practical pattern is:

  1. gather independent inputs outside the lock
  2. lock only the read-modify-write section
  3. release as soon as shared state is consistent again

This matters in browser apps, Node.js services, and any code that coordinates writes to memory, caches, or files.

Prefer simpler coordination when possible

Sometimes a lock is not the best fix. Alternatives include:

  • making operations idempotent
  • using atomic database updates
  • serializing through a single worker queue
  • avoiding shared mutable state entirely

A mutex is useful, but it should not be the first tool if the underlying design can be simplified.

Libraries can save time

If you need a tested implementation, libraries such as async-mutex provide lock primitives with clearer APIs and fewer edge-case mistakes than ad hoc queue code.

javascript
1import { Mutex } from "async-mutex";
2
3const mutex = new Mutex();
4
5await mutex.runExclusive(async () => {
6  // protected section
7});

That is often the right choice in production code unless the requirements are extremely small.

Common Pitfalls

  • Assuming JavaScript cannot have race conditions because it is single-threaded.
  • Locking too much code and accidentally turning unrelated async work into a bottleneck.
  • Forgetting to release the lock when an exception occurs, which is why finally matters.
  • Using a lock where a database transaction, queue, or redesign would solve the problem more cleanly.
  • Reimplementing a mutex casually in production code without testing cancellation and error paths.

Summary

  • JavaScript can still have async race conditions whenever awaited operations interleave around shared state.
  • An async lock is usually a promise-based queue that serializes access to a critical section.
  • Keep the locked section small and release it reliably with finally.
  • Consider simpler architectural alternatives before adding lock logic.
  • For production use, a well-tested mutex library is often safer than a homemade implementation.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.