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

async-cancelable-event

v1.0.2

Published

A web standard Event that can asynchronously preventDefault

Readme

async-cancelable-event

Async-aware cancelable events for the DOM — because preventDefault() shouldn't require synchronous timing.

npm version license: MIT

The Problem

In the DOM event model, event.preventDefault() must be called synchronously during listener execution for event.defaultPrevented to reflect the cancellation. This creates a fundamental problem for async listeners:

target.addEventListener('save', async (event) => {
  const confirmed = await checkWithServer(); // ← async work
  if (!confirmed) event.preventDefault();    // ← too late! dispatcher already moved on
});

By the time the await completes and the listener calls preventDefault(), the dispatching code has already read defaultPrevented and proceeded with the default action.

The Solution

AsyncCancelableEvent extends the standard Event class with a coordination mechanism between dispatchers and async listeners:

  • Dispatchers await the asyncDefaultPrevented property instead of reading defaultPrevented synchronously. This promise resolves only after all async listeners that registered a hold have completed.
  • Async listeners call withAsyncPreventDefault() to register an ongoing async operation, receiving a disposable handle. The handle's disposal signals that the listener's async work is complete. The listener can still call the standard preventDefault() at any point.

Installation

npm install async-cancelable-event

Usage

Dispatching

import AsyncCancelableEvent from 'async-cancelable-event';

const event = new AsyncCancelableEvent('save');
target.dispatchEvent(event);

if (await event.asyncDefaultPrevented) {
  // At least one listener called preventDefault()
  console.log('Save was cancelled');
} else {
  // No listener prevented the default action
  console.log('Proceeding with save');
}

Listening (recommended: using)

target.addEventListener('save', async (event) => {
  using _ = event.withAsyncPreventDefault();
  //    ↑ The dispatcher will now wait for this listener to complete

  const confirmed = await checkWithServer();
  if (!confirmed) event.preventDefault();

  // `using` ensures the hold is released even if an error is thrown
});

Listening (manual disposal)

If you need more control over when the hold is released, you can call dispose() manually:

target.addEventListener('save', async (event) => {
  const handle = event.withAsyncPreventDefault();

  try {
    const confirmed = await checkWithServer();
    if (!confirmed) event.preventDefault();
  } finally {
    handle?.dispose();
  }
});

Multiple async listeners

All async listeners that call withAsyncPreventDefault() are tracked. asyncDefaultPrevented waits for every hold to be released before resolving:

target.addEventListener('save', async (event) => {
  using _ = event.withAsyncPreventDefault();
  await validateForm();       // may call preventDefault()
});

target.addEventListener('save', async (event) => {
  using _ = event.withAsyncPreventDefault();
  await checkPermissions();   // may call preventDefault()
});

// asyncDefaultPrevented resolves only after BOTH listeners complete

Non-cancelable events

If the event is not cancelable, withAsyncPreventDefault() returns undefined — mirroring the standard behavior where preventDefault() is a no-op on non-cancelable events:

const event = new AsyncCancelableEvent('info', { cancelable: false });
event.withAsyncPreventDefault(); // → undefined

API

class AsyncCancelableEvent extends Event

constructor(eventName: string, init?: EventInit)

Creates a new AsyncCancelableEvent. The event is cancelable by default (cancelable: true), since the entire purpose of this class is to support async cancellation. Pass { cancelable: false } in init to override.

get asyncDefaultPrevented: Promise<boolean>

An asynchronously-resolved version of Event.defaultPrevented. Waits for all async listener holds to be released, then returns the final value of defaultPrevented.

  • If no async listeners registered a hold, resolves immediately with the current synchronous value.
  • If one or more holds are active, resolves only after all have been disposed.

⚠️ Always await this property. Reading it without await yields a Promise<boolean>, not a boolean — synchronous checks will always appear as if the event was not prevented.

withAsyncPreventDefault(): ExplicitlyDisposable | undefined

Registers an asynchronous hold on this event and returns a disposable handle. The handle must be released when the async listener's work is complete.

  • Returns undefined if the event is not cancelable.
  • The returned handle implements both dispose() (explicit) and Symbol.dispose (for using declarations).

type ExplicitlyDisposable = { dispose(): void } & Disposable

A type representing an object that is both explicitly disposable (via a dispose() method) and implicitly disposable (via Symbol.dispose, enabling using declarations).

How It Works

Under the hood, withAsyncPreventDefault() uses Promise.withResolvers() to create a promise/resolve pair. The promise is pushed into an internal registry, and the resolve function becomes the disposal callback. When asyncDefaultPrevented is accessed, it uses Promise.all() on the registry to wait for every hold to be released, then reads the standard defaultPrevented property.

withAsyncPreventDefault()  →  creates { promise, resolve }
                                promise  → pushed to #asyncHandlers
                                resolve  → returned as dispose / Symbol.dispose

asyncDefaultPrevented      →  Promise.all(#asyncHandlers)
                                .then(() => this.defaultPrevented)

This design means preventDefault() remains the single mechanism for cancellation — the hold system only controls timing, ensuring the dispatcher waits until async listeners have had their say.

Browser Support

This module relies on two relatively recent JavaScript features:

| Feature | Chrome | Firefox | Safari | Node.js | |---|---|---|---|---| | Promise.withResolvers() | 119+ | 121+ | 17.4+ | 22+ | | using / Symbol.dispose | 123+ | 119+ | Pending | 20.9+ |

As of early 2026, Chrome 123+ and Firefox 119+ support all of these features; Safari support for Explicit Resource Management is still pending Promise.withResolvers() has broader support, available since Chrome 119 and Firefox 121

If you need to support older browsers, you can:

  • For Promise.withResolvers: Use a polyfill — the function has a trivial one-line implementation.
  • For using / Symbol.dispose: Use manual dispose() calls instead of using declarations, and polyfill Symbol.dispose if needed. TypeScript has supported using and Disposable since version 5.2

License

Copyright 2026 Devin Weaver

Released under the MIT License.

  • Tests written by a human
  • Implementation written by a human
  • Documentation generated by an AI