Profiles·Public

p-queue

semver>=7.0.0 <10.0.0postconditions15functions9last verified2026-06-23coverage score100%

Postconditions — what we check

  • add · add-unhandled-rejection
    error
    Whenqueue.add(fn) is called without a .catch() on the returned Promise, and without await inside a try-catch block. If the task fn throws (or times out), the rejection is unhandled and crashes the Node.js process or triggers unhandledRejection.
    ThrowsError (or TimeoutError if timeout is configured)
    Required handlingCaller MUST handle errors from queue.add() via one of: 1. await queue.add(fn) inside try-catch 2. queue.add(fn).catch(handler) 3. Promise.all([queue.add(fn1), queue.add(fn2)]) inside try-catch The queue's 'error' event does NOT replace per-call error handling because "the promise returned by add() still rejects" even when an error event listener is registered. TimeoutError is thrown when tasks exceed the queue's timeout option.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilitysilent
    Sources[1][2]
  • addAll · addall-unhandled-rejection
    error
    Whenqueue.addAll(fns) is called without await in a try-catch or without .catch(). If any task in the array throws, the entire addAll() promise rejects with the first error. Remaining tasks still execute in the background, but the rejection is unhandled — crashes Node.js or triggers unhandledRejection.
    ThrowsError (same as add() — any error thrown by any task function)
    Required handlingCaller MUST wrap queue.addAll() in try-catch or chain .catch(): try { await queue.addAll(fns); } catch (error) { console.error('Batch failed:', error); } Note: even after catching, background tasks may still be executing. Use await queue.onIdle() if you need all tasks to fully settle.
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilitysilent
    Sources[1][3]
  • addAll · addall-partial-failure-background-tasks
    warning
    Whenqueue.addAll(fns) rejects on first task failure. Caller catches the rejection and continues, unaware that remaining tasks from the failed batch are still executing concurrently in the background, consuming queue concurrency slots.
    ThrowsNo additional error — remaining tasks silently complete or fail in background
    Required handlingAfter catching an addAll() rejection, call queue.clear() to prevent queued tasks from starting, and await queue.onIdle() to wait for already-running tasks to complete before proceeding: try { await queue.addAll(fns); } catch (error) { queue.clear(); // stop queued-but-not-started tasks await queue.onIdle(); // wait for in-flight tasks to settle throw error; }
    costmediumin prodsilent failureusers seedegraded performancevisibilitysilent
    Sources[3]
  • onIdle · onidle-resolves-once
    warning
    Whenawait queue.onIdle() is used inside a loop as a "wait for this batch to finish" primitive, expecting it to gate each iteration. Since onIdle() resolves only once per call, a subsequent call may return immediately if the queue is already idle, causing a batch to be skipped without any waiting.
    Required handlingUse queue.on('idle', handler) for repeated idle-detection callbacks. For one-shot shutdown: await queue.onIdle() is correct. For pacing batches: schedule tasks incrementally and use onSizeLessThan() for backpressure rather than waiting for full idle between batches.
    costlowin prodsilent failureusers seedegraded performancevisibilitysilent
    Sources[1]
  • onIdle · onidle-paused-queue-hangs
    error
    Whenqueue.pause() was called (or autoStart:false constructor option with no queue.start()), and queue.onIdle() is awaited. The queue never processes tasks, so pending tasks never complete, and onIdle() never resolves. There is no timeout built into onIdle().
    Required handlingAlways ensure the queue is started (queue.start() or autoStart:true default) before awaiting onIdle(). For shutdown sequences: queue.pause(); // stop accepting new tasks queue.clear(); // remove queued-but-not-started tasks await queue.onIdle(); // wait for in-flight tasks to drain Do NOT call queue.pause() before onIdle() if you still have pending tasks — pause() stops NEW tasks from starting but does not affect already-running tasks.
    costmediumin proddelayed failureusers seeservice unavailablevisibilityvisible
    Sources[1][3]
  • onError · onerror-does-not-replace-add-catch
    error
    Whenqueue.onError() is used as the only error handler — without also handling rejections from individual queue.add() calls. The add() promise still rejects independently; the onError() event-based handler does not absorb it. The README explicitly states: "The promise returned by add() still rejects. You must handle each add() promise to avoid unhandled rejections."
    Required handlingEach queue.add() call MUST have its own rejection handling even when using onError() for monitoring. Minimum pattern: queue.add(() => riskyTask()).catch(() => {}); // suppress per-add rejection // AND monitor globally: Promise.race([queue.onError(), queue.onIdle()]) .catch(err => { queue.pause(); reportError(err); });
    costmediumin prodimmediate exceptionusers seeservice unavailablevisibilitysilent
    Sources[1][3]
  • onError · onerror-never-resolves-hangs
    warning
    Whenawait queue.onError() is used without being raced against queue.onIdle(). queue.onError() returns Promise<never> — it NEVER resolves normally. In a success scenario (no task ever errors), awaiting it blocks forever.
    Required handlingAlways race onError() against onIdle(): try { await Promise.race([queue.onError(), queue.onIdle()]); } catch (error) { queue.pause(); throw error; } Never await queue.onError() in isolation.
    costmediumin proddelayed failureusers seeservice unavailablevisibilityvisible
    Sources[1]
  • onEmpty · onempty-not-all-done
    warning
    Whenawait queue.onEmpty() is used as a "wait for all tasks to complete" signal, then the caller proceeds assuming all work is finished. In fact, tasks already dequeued and executing (queue.pending > 0) continue to run after onEmpty() resolves. The README states: "onEmpty merely signals that the queue is empty, but some promises haven't completed yet."
    Required handlingUse await queue.onIdle() (not onEmpty()) to wait for ALL work to finish: await queue.onIdle(); // size === 0 AND pending === 0 Use onEmpty() only when you specifically need to know when the queue has been fully dequeued but don't care about in-flight tasks completing.
    costmediumin prodsilent failureusers seelost datavisibilitysilent
    Sources[1]
  • onSizeLessThan · onsizelessthan-excludes-pending
    warning
    Whenqueue.onSizeLessThan(limit) is used to enforce a hard cap on total concurrent work (queued + running combined). Since it only checks queue.size (tasks waiting to start) and not queue.pending (tasks currently executing), the total in-flight work can be up to limit + concurrency simultaneously. The README notes: "There could still be up to concurrency jobs already running that this call does not include in its calculation."
    Required handlingTo limit TOTAL concurrent work (queued + running), check both: await queue.onSizeLessThan(Math.max(0, limit - queue.pending)); Or use a lower limit that accounts for the concurrency headroom. For simple backpressure (just prevent unbounded queue growth), onSizeLessThan(N) is correct and sufficient.
    costlowin prodsilent failureusers seedegraded performancevisibilitysilent
    Sources[1]
  • onPendingZero · onpendingzero-ignores-queued-tasks
    warning
    Whenawait queue.onPendingZero() is used as a "wait for all work to finish" signal — the same way queue.onIdle() is used. Since onPendingZero() only waits for currently RUNNING tasks (queue.pending === 0) and ignores QUEUED tasks (queue.size > 0), the promise resolves while items are still sitting in the queue waiting to start. The documentation states: "The difference with onIdle is that onPendingZero only waits for currently running tasks to finish, ignoring queued tasks." Common mistake: using onPendingZero() in shutdown/completion checks expecting all work to be done, when in fact a full queue of pending tasks remains unexecuted.
    ThrowsNo exception — the method always resolves (no rejection path). The problem is silent early resolution: code continues as if all work is done, while queued tasks have not yet been started or executed. This is a Nark profile violation, not a thrown error.
    Required handlingUse await queue.onIdle() when you need ALL work (queued + running) to complete: await queue.onIdle(); // size === 0 AND pending === 0 Use await queue.onPendingZero() ONLY when you specifically want to drain in-flight tasks while keeping queued tasks held (e.g., after queue.pause()): queue.pause(); await queue.onPendingZero(); // All running tasks finished; queued tasks are still waiting // Safe to mutate shared state now NEVER use onPendingZero() as a replacement for onIdle() in shutdown sequences: // WRONG — resolves even if 1000 tasks are still queued: await queue.onPendingZero(); process.exit(0); // queued tasks never ran! // CORRECT — waits for queue AND pending to drain: await queue.onIdle(); process.exit(0);
    costmediumin prodsilent failureusers seelost datavisibilitysilent
    Sources[1][2]
  • onPendingZero · onpendingzero-resolves-immediately-when-no-running-tasks
    warning
    Whenqueue.onPendingZero() is called when no tasks are currently running (queue.pending === 0) — even if the queue has many items waiting to start (queue.size > 0). The method returns immediately via its fast-path check without waiting for any work to be done. This is an easy mistake when the queue is paused with autoStart:false and tasks have been added but not yet started — pending is 0, so onPendingZero() resolves instantly even though no work has been done. Implementation confirmed from source: "if (this.#pending === 0) { return; }"
    ThrowsNo exception — resolves immediately (synchronous early return). The problem is that code after await onPendingZero() runs instantly even if the queue has work that has never started.
    Required handlingOnly use onPendingZero() when you know tasks are currently running and you want to wait for them to drain. Use queue.pending to check before calling: // If you want to drain only currently-running tasks: if (queue.pending > 0) { await queue.onPendingZero(); } // Note: this does NOT process queued tasks // If you want ALL work done (queued + running), always use onIdle(): await queue.onIdle(); // handles the already-idle case internally too
    costlowin prodsilent failureusers seedegraded performancevisibilitysilent
    Sources[2]
  • onRateLimit · onratelimit-no-intervalcap-never-resolves
    warning
    Whenawait queue.onRateLimit() is called on a PQueue instance that was constructed without an `intervalCap` (or with `intervalCap: Infinity`, the default). The queue can never enter the rate-limited state — `isRateLimited` is always false — so the promise never resolves. There is no built-in timeout. The producer task that awaited onRateLimit() hangs forever, leaking memory and event-loop slots, and downstream consumers stop receiving new work.
    Required handlingOnly call queue.onRateLimit() on a queue that has BOTH `intervalCap` AND `interval` configured to finite values: const queue = new PQueue({intervalCap: 5, interval: 1000}); await queue.onRateLimit(); // safe — queue can enter rate-limited state For queues without intervalCap, do not call onRateLimit(). Use queue.onSizeLessThan() for size-based backpressure instead: const queue = new PQueue({concurrency: 10}); await queue.onSizeLessThan(100); // size-based backpressure (no rate-limit needed) If a queue is constructed dynamically and may or may not have intervalCap, guard the await with a feature check or race against a timeout: await Promise.race([ queue.onRateLimit(), new Promise((_, reject) => setTimeout(() => reject(new Error('onRateLimit timed out')), 30_000)) ]);
    costmediumin proddelayed failureusers seeservice unavailablevisibilitysilent
    Sources[1][2]
  • onRateLimit · onratelimit-resolves-immediately-misses-transition
    warning
    Whenqueue.onRateLimit() is called when the queue is already rate-limited (queue.isRateLimited === true) — for example, called inside a tight producer loop that adds tasks faster than the interval budget allows. The method takes the fast-path early return and resolves IMMEDIATELY without waiting for any state change. Code expecting to "wait for the next rate-limit event" continues without pause and the loop floods the queue with more tasks, defeating the intended backpressure pattern.
    Required handlingUse queue.onRateLimit() to wait for the FIRST transition into rate-limited state, not as a per-iteration gate. For per-iteration backpressure during an ongoing rate-limited period, await queue.onRateLimitCleared() (wait for the rate-limit to lift) before scheduling the next batch: // CORRECT — drain pattern: for (const task of bigBatch) { if (queue.isRateLimited) { await queue.onRateLimitCleared(); } queue.add(() => doWork(task)).catch(() => {}); } // WRONG — onRateLimit() inside the loop returns instantly once rate-limited: for (const task of bigBatch) { await queue.onRateLimit(); // returns immediately every iteration queue.add(() => doWork(task)).catch(() => {}); }
    costlowin prodsilent failureusers seedegraded performancevisibilitysilent
    Sources[1][2]
  • onRateLimitCleared · onratelimitcleared-paused-queue-hangs
    error
    Whenawait queue.onRateLimitCleared() is called on a queue that is rate-limited AND has been paused via queue.pause() (or constructed with autoStart: false and never started). The 'rateLimitCleared' event is emitted from the internal #next() method, which only runs when the queue is processing tasks. A paused queue never processes tasks, so the rate-limit window never elapses from the queue's perspective, the event never fires, and the awaited promise hangs forever. There is no built-in timeout.
    Required handlingAlways ensure the queue is processing (queue.start() called, or autoStart default true and queue.pause() NOT in effect) before awaiting onRateLimitCleared(). For pause-then-resume patterns: queue.pause(); // ... do other work ... queue.start(); // resume processing FIRST await queue.onRateLimitCleared(); // now safe to await For shutdown sequences, do not use onRateLimitCleared() at all — use queue.clear() + await queue.onIdle() to fully drain instead. If the queue MAY be paused at call time, race against a timeout: await Promise.race([ queue.onRateLimitCleared(), new Promise((_, reject) => setTimeout(() => reject(new Error('onRateLimitCleared timed out')), 30_000)) ]);
    costmediumin proddelayed failureusers seeservice unavailablevisibilityvisible
    Sources[1][2]
  • onRateLimitCleared · onratelimitcleared-resolves-immediately-when-not-rate-limited
    warning
    Whenqueue.onRateLimitCleared() is called when the queue is NOT currently rate-limited (queue.isRateLimited === false) — for example, called in a producer that wraps every add() with a "wait until rate-limit clears" guard. The method takes the fast-path early return and resolves IMMEDIATELY. Code expecting to "wait for the next rate-limit-cleared transition" continues without any backpressure delay, and on the very next add() the rate-limit may engage again, requiring another guard, and so on — the backpressure intent is silently bypassed.
    Required handlingUse queue.onRateLimitCleared() only INSIDE a rate-limited window, gated on queue.isRateLimited: if (queue.isRateLimited) { await queue.onRateLimitCleared(); } queue.add(() => doWork()).catch(() => {}); For unconditional per-task pacing, use a different primitive such as p-throttle or a manual setTimeout — onRateLimitCleared() is event-based and only delays when an actual rate-limit window is active. DO NOT pattern an entire producer loop as: // WRONG — first iteration resolves instantly, no pacing applied: for (const task of bigBatch) { await queue.onRateLimitCleared(); queue.add(() => doWork(task)).catch(() => {}); }
    costlowin prodsilent failureusers seedegraded performancevisibilitysilent
    Sources[1][2]

Sources

Every postcondition cites at least one of these. Numbered to match the footnotes above.

  1. [1]raw.githubusercontent.com/sindresorhus/p-queuehttps://raw.githubusercontent.com/sindresorhus/p-queue/main/readme.md
  2. [2]raw.githubusercontent.com/sindresorhus/p-queuehttps://raw.githubusercontent.com/sindresorhus/p-queue/main/source/index.ts
  3. [3]github.com/sindresorhus/p-queuehttps://github.com/sindresorhus/p-queue/blob/main/source/index.ts
Need a different package?
Request a profile