npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@debonet/es6pledges

v3.0.0

Published

Cancellable Promises for Javascript ES6

Readme

es6pledges

Releasable Promises for JavaScript ES6 (also known as "cancellable promises").

TL;DR

  • A Pledge is a Promise subclass whose instances can be released from their obligation through a consent-based release pipeline.
  • new Pledge( fxExecutor ) takes ONE argument, exactly like Promise. The executor's RETURN VALUE is the release policy.
  • pledge.release( xReason ) asks the policy for consent: the policy may clean up and consent, or refuse. No forced settlement exists anywhere.
  • The direction of settlement is decided by type dispatch on xReason: PactResolve and PactReject wrappers force direction, a bare Error rejects, anything else resolves.
  • resolve() / reject() / fulfill() are refusable sugar over release().
  • Cancel a pipeline at its SOURCE. Releasing a derived pledge affects only that pledge; release never propagates upstream.
  • releaseOn( signal ) adapts an AbortSignal into a release request.
  • Dependency-free at runtime.
  • v3 is breaking. See "Migration from v2" below.

Usage

const Pledge = require( "@debonet/es6pledges" );

The primary example is a delay with cleanup: an interval ticks toward a deadline, and the executor returns the release policy — a cleanup function that stops the interval.

function fpledgeDelay( dtm, dtmTick = 100 ){
	return new Pledge(( fResolve, fReject ) => {
		let dtmElapsed = 0;
		const interval = setInterval(() => {
			dtmElapsed += dtmTick;
			if ( dtmElapsed >= dtm ){
				clearInterval( interval );
				fResolve( dtm );
			}
		}, dtmTick );
		return () => clearInterval( interval );
	});
}

Usage — hold the SOURCE pledge, per the cancel-at-source idiom; do not release a derived pledge here:

const pledgeDelay = fpledgeDelay( 10000 );

pledgeDelay.then(( x ) => console.log( "result:", x ));

setTimeout(() => { pledgeDelay.resolve( "faster" ); }, 250 );

Output:

result: faster

The executor runs exactly as in a native Promise. Whatever it returns becomes the release policy. When resolve() is called, the policy runs, clears the interval, consents by fulfilling, and the pledge resolves with the given value.

This example is shared across @debonet/es6pledges, @debonet/es6tasks, and @debonet/es6pacts, so the composition is visible by inspection. This is the delay WITH cleanup and WITHOUT reporting. The diff contract: adding a third executor parameter fReport and the single body line fReport( dtmElapsed ); yields es6tasks' ftaskDelay reporting lines inside es6pacts' fpactDelay; this form plus those two additions IS fpactDelay (@debonet/es6pacts). All other lines are identical across the three libraries.

Cancel at the source

Release is scoped to the pledge it is called on. Releasing a derived pledge (one produced by then / catch / finally) settles only that pledge and deterministically skips its own stage handler. It never settles, rejects, or otherwise affects an ancestor: ancestors may have other consumers, and a derived pledge does not own them.

To cancel a whole pipeline, hold the SOURCE and release it. Cancellation then flows DOWNSTREAM as ordinary settlement:

// v2: hold the end of the chain, release it, everything unwinds
const pledge = fpledgeFetch( sUrl ).then( fParse ).then( fRender );
pledge.reject( "navigated away" );

// v3: hold the SOURCE; cancellation flows DOWNSTREAM as ordinary
// settlement
const pledgeSource = fpledgeFetch( sUrl );
const pledgeDone = pledgeSource.then( fParse ).then( fRender );
pledgeSource.reject( "navigated away" );
  • A reject-flavored release of the source rejects it; the rejection skips downstream then-handlers natively and reaches every descendant's catch.
  • A resolve-flavored release of the source runs downstream then-handlers with the early value. This is ordinary promise semantics, and correct: the consumer asked for an early result, and the pipeline processes it as it would any result.

The release policy

The executor's return value is the release policy. It takes one of three forms:

  • A function ( xReason ) => ... — the release handler. Its signature is ( xReason ) only; it closes over fResolve / fReject and all executor locals. This is the useEffect-cleanup idiom.
  • An Error instance — unconditional refusal: every release() rejects with that Error and the pledge is untouched.
  • undefined or any other value — releasable with no cleanup: release settles the pledge via type dispatch. Accidental non-function returns (a number, a string, an object) are not errors; they mean "no cleanup".

An external or shared release policy needs no special support — wrap it in scope:

return ( xReason ) => fExternal( fResolve, fReject, xReason );

Async executors: acquire and return

With an async executor, the policy arrives when the executor's returned promise resolves. A release() requested before then WAITS for executor completion: you cannot release a pledge before its executor finishes. Async executors should therefore acquire-and-return — acquire resources, start the work, and return the policy quickly:

new Pledge( async ( fResolve, fReject ) => {
	const connection = await fpconnectionOpen( sHost );
	fpRun( connection ).then( fResolve, fReject );
	return () => connection.close();
});

Doing the whole work inside the executor's await chain makes the pledge unreleasable in practice.

The release pipeline

pledge.release( xReason ) proceeds:

  1. Already settled — the returned promise fulfills with the settlement record (trivial success).
  2. Wait for the policy (the async-executor gap).
  3. Policy is an Errorrelease()'s promise rejects with it; the pledge is untouched.
  4. Policy is a function — invoke it with xReason and await its return unconditionally (synchronous returns are normalized; there is exactly one handler flavor, promise-semantics).
    • Handler fulfills — release SUCCEEDED. If the handler already settled the pledge (via closured fResolve / fReject), done. Otherwise the pledge auto-settles by type dispatch on xReason. The handler's fulfillment value is discarded — there is no return-value protocol.
    • Handler rejects or throws — release REFUSED: the pledge is untouched, all handlers stay attached, and release()'s promise rejects with the handler's reason. Precedence: if the pledge settled anyway during the await (the work finished mid-refusal), release() fulfills with the settlement record — refusal after settlement is moot.
  5. Policy is any other value — auto-settle by type dispatch directly.
  6. A release handler that never settles stalls the pipeline. "Must settle" is an author obligation — the same trust extended to an executor that never resolves.

Type dispatch

The direction of auto-settle is decided by what xReason IS:

  • xReason instanceof PactResolve — resolve with xReason.cause
  • xReason instanceof PactReject — reject with xReason.cause
  • xReason instanceof Error — reject with xReason
  • anything else — resolve with xReason

Rules:

  • The wrapper checks precede the bare-Error check (the wrappers extend Error).
  • Unwrapping happens exactly once. No recursion: new PactResolve( new PactReject( x )) resolves with the inner PactReject instance.
  • A bare foreign controller.abort() produces an AbortError DOMException, which rejects, matching platform semantics. controller.abort( "value" ) passes the raw value, which resolves. AbortSignal.timeout()'s reason is a TimeoutError, which rejects.

Wrappers: PactResolve and PactReject

const { PactResolve, PactReject } =
	require( "@debonet/es6pledges/wrappers" );

Also available as statics: Pledge.PactResolve, Pledge.PactReject.

They exist for direction-forcing through an untyped channel: rejecting with a non-Error, resolving with an Error, and any release intent that must travel through a signal. They extend Error deliberately: stack capture at the release call site (debugging "who cancelled me"), and they are exception-shaped if they leak to foreign signal consumers. The wrapped value travels in .cause.

release() return contract

release( xReason ) returns a promise that:

  • FULFILLS with a settlement record { sStatus, x } (sStatus is "resolved" or "rejected", x the value or reason) AFTER the pledge settles, ordered after handlers registered before the release() call. It never rejects because the pledge rejected (allSettled-style): releasing means you caused the outcome; you are not forced to catch it.
  • REJECTS only on refusal while the pledge was still pending.

Guaranteed ordering:

pledge.then(() => console.log( "here1" ));
pledge.release( "x" ).then(() => console.log( "here2" ));

// always logs here1 then here2

Repeat and concurrent release

Release is idempotent. The first pipeline wins; later release() calls made while a pipeline is in flight JOIN the in-flight outcome: they return promises that settle identically to the first call's promise, and their xReason is discarded. After a refusal completes, the in-flight slot clears and a new release() starts a fresh pipeline — refusal is not permanent unless the policy is an Error. After a success the pledge is settled, so later calls fulfill with the settlement record.

Settle verbs

Sugar through the SAME pipeline — no forced settlement exists anywhere:

  • pledge.resolve( x )pledge.release( new PactResolve( x ))
  • pledge.reject( e )pledge.release( new PactReject( e ))
  • pledge.fulfill( x ) — alias of resolve( x ), kept for v2 continuity.

All are refusable like any release.

Design principle: settlement is a REPORT that the obligation ended; nothing may emit it without the release handler's consent. This is the "robot arm" principle — settling while the physical work continues makes the promise lie to its consumers. Callers who want to stop caring do not need settlement: abandonment is not awaiting, or a consumer-side Promise.race.

Note the instance / static name collision, unchanged from v2: the instance method pledge.resolve() requests early release of an existing Pledge, while the inherited static Pledge.resolve() constructs an already-resolved Pledge. Likewise pledge.reject() versus Pledge.reject().

releaseOn( signal )

The only place AbortSignal appears in the API:

const pledge = fpledgeWork().releaseOn( controller.signal );

When the signal aborts, this.release( signal.reason ) runs. A TimeoutError from AbortSignal.timeout() then rejects via type dispatch with zero timeout-specific code. Returns this for chaining.

This is a refusable REQUEST, like any release. A consumer needing an unconditional deadline races at the consumer layer instead.

Outbound cancellation needs no API: the producer creates an AbortController inside the executor and returns ( x ) => controller.abort( x ).

Chains: the ownership principle

A pledge may only release what it OWNS. release() affects the pledge it is called on, plus at most one owned child: an in-flight pledge returned by that stage's own handler. Ancestors are not owned — they may have other consumers — so upstream propagation does not exist.

  • Derived pledges have the default policy. Pledges produced by then / catch / finally are releasable with no cleanup, unless their stage owns in-flight work (below).
  • Releasing a derived pledge before its parent settles: the derived pledge settles NOW by type dispatch, and when the parent later settles, the stage's handler is SKIPPED — deterministically, not by timing inference.
  • The one owned child: if the stage's handler already ran and returned an in-flight pledge, releasing the derived pledge releases that child through the child's own pipeline. Child consent: the child settles, the derived pledge adopts its settlement, and release() fulfills with the settlement record. Child refusal: release() rejects with the child's refusal reason and both pledges are untouched. Delegation is one level from the caller's perspective; a child with its own owned child recurses under the same rule.
  • A handler that returned a plain promise (not a Pledge) owns nothing releasable: the default policy applies, the derived pledge settles by dispatch, and the plain promise's later settlement is discarded. Non-Pledge work is uncancellable; the release is honest about it.

Edge to know about: if the parent later REJECTS and the released derived pledge was its only consumer, the platform may emit an unhandled-rejection warning. Consumers who care can attach their own no-op catch to the parent.

Combinators

Pledge.all / any / race / allSettled work as in Promise, and the combined pledge is releasable with no cleanup. Members are caller-supplied and possibly shared — NOT owned by the combined pledge. Releasing the combined pledge settles the combined pledge only, via the standard pipeline, and touches no member. Members keep running to their real settlements, which are then discarded.

To stop members you hold, release them yourself:

vpledge.forEach(( pledge ) => pledge.release( xReason ));

Each member runs its own pipeline; each refusal is individually visible on its own release() promise.

  • allSettled: releasing the combined pledge settles the aggregate directly per dispatch. No synthesized member records exist.
  • race([]) and other never-settling combinations are unremarkable: release settles the combined pledge by dispatch; nothing hangs.
  • A combined pledge in a chain is an ordinary parent; the chain rules apply unchanged.

Statics

  • Pledge.resolve( x ) / Pledge.reject( e ) — inherited already-settled construction; a subsequent release() fulfills with the settlement record.
  • Pledge.detachable( p ) — wraps a foreign promise in a releasable-with-no-cleanup Pledge. Releasing it orphans p: p keeps running, and its settlement is discarded.

Building your own releasable class

const fclassReleasable =
	require( "@debonet/es6pledges/fclassReleasable" );

const Releasable = fclassReleasable( classPromiseCompatible );

fclassReleasable( classPromise ) produces a releasable subclass of any Promise-compatible class. Pledge is fclassReleasable( Promise ). Also exported as @debonet/es6pledges/makereleasable.

Migration from v2

v3 is breaking (version 3.0.0).

  • Chain-wide release is removed. v2's documented behavior — "when a chain of pledges is released, ALL of the .then() clauses in the chain also get released" — no longer exists. Release is scoped to the pledge it is called on (plus a handler-returned child). Cancel a pipeline at its SOURCE; see "Cancel at the source" above.
  • Constructor: new Pledge( fxExecutor, fOnRelease ) becomes new Pledge( fxExecutor ). Port by moving the cleanup into the executor's return value:
// v2
new Pledge(
	( fResolve ) => { timeout = setTimeout( fResolve, dtm ); },
	() => clearTimeout( timeout )
);

// v3
new Pledge(( fResolve ) => {
	const timeout = setTimeout( fResolve, dtm );
	return () => clearTimeout( timeout );
});
  • Release handler signature: v2's ( fResolve, fReject, bResolve, xStatus, ...vx ) becomes ( xReason ) only. The handler closes over fResolve / fReject and executor locals.
  • release() arguments: release( bResolve, xStatus, ...vx ) becomes release( xReason ). The bResolve flag is removed; direction comes from type dispatch on the reason. Use PactResolve / PactReject to force direction. Extra variadic arguments are removed.
  • Veto: v2 cancelled the release when the handler returned truthy without settling. v3 refuses by rejecting or throwing from the handler; a fulfilled handler always consents; the fulfillment value is discarded.
  • release() return: never true / Promise<boolean>. Always a promise: fulfills with a settlement record on success, rejects on refusal.
  • Combinator release: v2 released every member. v3 releases the combined pledge only; no member is touched.
  • allSettled release: v2 synthesized released-fulfilled / released-rejected pseudo-records. v3 settles the aggregate per dispatch; members run to their real settlements; no pseudo-records exist.
  • race: v2 appended a hidden extra member; v3 does not.
  • New API: releaseOn( signal ), PactResolve, PactReject (statics on the class and via the "./wrappers" export).
  • Kept: resolve / reject / fulfill verbs (now refusable sugar over release()), detachable( p ), and the instance-versus-static resolve / reject distinction.