JS · Concurrency · AsyncJan 28, 2026 · 1 min read

Simple Semaphores

A minimal Semaphore class in JavaScript: acquire blocks when no permits are available, release hands permits directly to waiting callers, and a run helper wraps both with try/finally.

JavaScript
export default class Semaphore {
  constructor(maxPermits = 1) {
    if (!Number.isInteger(maxPermits) || maxPermits < 1) {
      throw new Error("maxPermits must be an integer >= 1");
    }
    this.maxPermits = maxPermits;
    this.available = maxPermits;
    this.waiters = [];
  }

acquire() {
    if (this.available > 0) {
      this.available--;
      return Promise.resolve();
    }

return new Promise(resolve => {
      this.waiters.push(resolve);
    });
  }

release() {
    if (this.waiters.length > 0) {
      // Hand permit directly to next waiter
      const next = this.waiters.shift();
      next();
      // No need to increment available
    } else {
      // No waiters, return permit to pool
      // this.available++; // if released is called too many times

// clamp on release to prevent permit inflation if release is called too many times.
      // dont call x++ because of post-increment semantics
      this.available = Math.min(this.available + 1, this.maxPermits);
    }
  }

// Optional helper
  async run(fn, ...args) {
    await this.acquire();
    try {
      return await fn(...args);
    } finally {
      this.release();
    }
  }
}