mrkeyoor.com_
Thu 06 Aug 02:41 UTC
npmUtilsupdated 06 Aug 2026

p-timeout

p-timeout takes a promise and gives you back a decorated promise that rejects with a TimeoutError if the original has not settled within a given number of milliseconds. You can swap the rejection for a fallback function, a custom error, or a plain resolve with undefined, and you can abort the wrapper early with an AbortSignal. It is about 700 bytes gzipped with zero dependencies. One thing it explicitly does not do: it never cancels the work you wrapped. The underlying promise keeps running, keeps holding memory, and keeps its side effects. p-timeout only stops you waiting for it. Its 45 million weekly downloads come almost entirely from being a transitive dependency of build tools and test runners rather than from people installing it directly.

Verdict

Well built and tiny, but for most new code AbortSignal.timeout() does the same job with no dependency and actually stops the work. Reach for p-timeout when you are bounding a promise from an API that gives you no cancellation hook, and accept that the operation keeps running behind your back.

API stability4/5The options object shape has been stable since v5 in 2022 and v7 changed only the Node floor. The catch is that major versions regularly bump engines and module format, so upgrading is more about your runtime than your call sites
Docs5/5A single README that documents every option with a runnable example, plus an unusually honest section telling you to consider AbortSignal.timeout() instead
Maintenance3/5Zero open issues and zero open PRs, but the last release and last push were both October 2025. That is finished-software quiet rather than abandoned, though nobody should expect fast turnaround on a report
Ecosystem4/545M weekly downloads and part of the widely used sindresorhus promise-fun family, but only 305 stars: the traffic is transitive, coming through other packages rather than direct adoption

Use it if

  • You need a deadline on a promise from an API that accepts no AbortSignal, such as an older database driver, a callback wrapped with promisify, or a third-party SDK method
  • You want a fallback on timeout rather than a rejection: options.fallback runs your function (retry, cached value, degraded response) instead of throwing
  • You need the timer itself to be controllable, either cleared early with the returned promise's .clear() method or swapped out via customTimers so sinon.useFakeTimers() does not freeze your production timeout
  • You want a typed TimeoutError class you can instanceof against and subclass, instead of matching on error message strings
Skip it if

Setup reality

npm install p-timeout, no peer dependencies, no build step, TypeScript types included. The friction is module format and timers. Since v5 the package is ESM only with an exports map, so CommonJS projects and older Jest setups need either transformIgnorePatterns tweaks or a dynamic import(); v7 also sets engines.node to >=20, which quietly breaks CI runners still on Node 18. If your test suite calls sinon.useFakeTimers() or jest.useFakeTimers(), the internal setTimeout gets frozen and your timeout never fires, so you have to capture the real timer functions before faking and pass them through options.customTimers. Finally, remember to call .clear() on the returned promise in long-lived processes, otherwise the pending timer keeps the event loop alive until it expires.

Patterns

Reject a promise after a deadlinebasic-timeout

import pTimeout from 'p-timeout';

try {
  const result = await pTimeout(slowOperation(), {milliseconds: 5000});
  console.log(result);
} catch (error) {
  console.error(error.message);
  //=> 'Promise timed out after 5000 milliseconds'
}

slowOperation() keeps running after the rejection; p-timeout only stops your await, it does not cancel anything.

Tell a timeout apart from a real failurecatch-timeout-error

import pTimeout, {TimeoutError} from 'p-timeout';

try {
  await pTimeout(fetchReport(), {milliseconds: 2000});
} catch (error) {
  if (error instanceof TimeoutError) {
    return {status: 'degraded'};
  }
  throw error;
}

Always branch on instanceof TimeoutError rather than the message string, which is user-configurable.

Set your own timeout message or errorcustom-message

await pTimeout(uploadFile(), {
  milliseconds: 10_000,
  message: 'Upload did not finish in 10s',
});

// or throw a specific error instance
class UploadTimeout extends TimeoutError {
  name = 'UploadTimeout';
}

await pTimeout(uploadFile(), {
  milliseconds: 10_000,
  message: new UploadTimeout('upload stalled'),
});

Subclass TimeoutError rather than Error for custom types, so callers checking instanceof TimeoutError still match.

Resolve with undefined instead of throwingresolve-instead-of-reject

const banner = await pTimeout(fetchBanner(), {
  milliseconds: 300,
  message: false,
});

if (banner === undefined) {
  // render without the banner
}

message: false is the only way to get a non-throwing timeout; the value is always undefined, so you cannot distinguish it from a genuine undefined result.

Run a fallback when the deadline passesfallback-on-timeout

const data = await pTimeout(fetchFromPrimary(), {
  milliseconds: 1000,
  fallback: () => fetchFromReplica(),
});

fallback may return a value or a promise; if the fallback itself hangs it has no deadline of its own, so wrap it in another pTimeout when that matters.

Clear the timer so it stops holding the event loop openclear-pending-timer

const promise = pTimeout(job(), {milliseconds: 60_000});

process.on('SIGTERM', () => {
  promise.clear();
});

await promise;

A pending 60 second timer keeps a Node process alive even after the wrapped promise settles early; .clear() releases it.

Abort the wait early with an AbortSignalabort-the-wrapper

import pTimeout from 'p-timeout';

const controller = new AbortController();
document.querySelector('#cancel')
  .addEventListener('click', () => controller.abort());

await pTimeout(longTask(), {
  milliseconds: 30_000,
  signal: controller.signal,
});

The signal aborts your wait, not longTask(). To actually stop the work, the task has to accept the signal itself.

Turn the timeout off with a config valuedisable-timeout

const timeoutMs = config.timeoutMs ?? Number.POSITIVE_INFINITY;

await pTimeout(job(), {milliseconds: timeoutMs});

Passing Infinity means never time out, which is handy for a single code path that has to support an opt-out.

Survive fake timers in testsfake-timers

import pTimeout from 'p-timeout';
import sinon from 'sinon';

const realSetTimeout = setTimeout;
const realClearTimeout = clearTimeout;

sinon.useFakeTimers();

await pTimeout(doSomething(), {
  milliseconds: 2000,
  customTimers: {setTimeout: realSetTimeout, clearTimeout: realClearTimeout},
});

Capture the real timer functions before installing fake timers, otherwise the internal setTimeout is frozen and the timeout never fires.

Skip the dependency when the callee accepts a signalnative-abortsignal-timeout

// No p-timeout needed: fetch aborts the actual request.
const response = await fetch('/api/report', {
  signal: AbortSignal.timeout(5000),
});

// Combining a deadline with a manual cancel:
const controller = new AbortController();
const signal = AbortSignal.any([
  controller.signal,
  AbortSignal.timeout(5000),
]);

AbortSignal.timeout and AbortSignal.any are built into Node 20 and current browsers; the aborted call actually stops and frees its resources.

Import it from a CommonJS fileesm-only-import

// CJS file: a static require() throws ERR_REQUIRE_ESM on older Node.
async function withDeadline(promise, ms) {
  const {default: pTimeout} = await import('p-timeout');
  return pTimeout(promise, {milliseconds: ms});
}

module.exports = {withDeadline};

v5 and later are ESM only; dynamic import is the portable escape hatch when you cannot convert the file.

Alternatives

PackageRegistryPick it when
abort-utilsnpmYou want native AbortSignal semantics and need to merge a timeout signal with a user-triggered cancel button
p-retrynpmThe real problem is a flaky operation that should be retried with backoff, not one that should fail after a fixed deadline
promise-timeoutnpmYou are stuck on CommonJS and cannot consume an ESM-only package