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

rhodium

v0.6.3

Published

A TypeScript `Promise` wrapper that adds syntax sugar.

Readme

License Build status JSR Version NPM Version NPM Downloads

Rhodium

About

Rhodium is a TypeScript-first Promise alternative with error tracking, cancellation, common async utilities, and a sprinkle of syntax sugar.

It uses native Promise internally, resulting in minimal performance loss.

Rhodium implements all Promise's static and non-static methods, making them cancellable and error-tracking as well.

import * as Rh from "rhodium" // Static methods
import Rhodium from "rhodium" // Class

Rhodium depends on nothing but ES2020, and AbortSignal with AbortController.

Table of contents

Interoperability with Promise

  • Rhodium is awaitable at runtime, and Awaited<T> can be used to await it in types.
  • Promise, or any PromiseLike, can be converted to Rhodium by passing it to Rhodium.resolve() or new Rhodium()
  • Rhodium is convertible to Promise. Simply get the promise property of a Rhodium instance.

[!NOTE] Conversion to Promise loses cancelability and other Rhodium-exclusive features.

Features

Cancellation

Cancellation prevents any further callbacks from running.

[!IMPORTANT] finally is completely unaffected by the cancellation feature. This callback will always execute, and it can be attached to a cancelled Rhodium.

Here is an example:

const myRhodium = Rhodium
    .try(() => console.log(1))
    .then(() => Rhodium.sleep(1000))
    .then(() => console.log(2))
    .finally(() => console.log(3))

This should print 1, 2 and 3, right? And it does!

However, if we append this line:

setTimeout(() => myRhodium.cancel(), 500)

...then suddenly only 1 and 3 are printed. Invocation of cancel has prevented the second console.log!

[!IMPORTANT]

  • cancel returns a Rhodium, but it is actually synchronous at its core. Once cancel is run, its effects are immediate.
  • If Rhodium rejects right before cancellation, the reason might get suppressed. If Rhodium rejects during cancellation, the reason gets caught into the returned value.

In addition, the described below limitation causes cancel to return a rejecting Rhodium, to preserve the ease of handling errors.

Limitations

[!WARNING]

  • Only the last Rhodium in a given chain can be cancelled. Cancelling in the middle is not allowed.
  • It is impossible to attach a new callback to a cancelled Rhodium, because no callbacks would run off of it. This situation throws synchronously, as deemed unintentional by the programmer. A check using cancelled property is possible, if that is the intended behaviour.

But what about multiple then calls on a single Rhodium?

A Rhodium or a Promise chain is not really a chain - it is a tree. What happens to the other branches when one gets cut off?

In that case, a branch gets cancelled all the way up until it meets another branch. Suppose you have a following tree of Rhodiums:

A -> B -> C -> D
       \> E -> F -> G

Only D and G would be cancellable, because they are at the ends of their chains. There are 3 possibilities to consider:

  • When D gets cancelled, so does C.
  • When G gets cancelled, so do F and then E.
  • Only when both D and G get cancelled, no matter the order, do B and A get cancelled as well.

Finalization

Rhodium.oneFinalized

Also known as the resolution value of cancel. Has a non-static shorthand called Rhodium.finalized.

The returned Rhodium is resolved once

  • this Rhodium had settled, or
  • the currently running callback and all of the following finally callbacks of the cancelled chain had been executed.
Rhodium
  .sleep(100)
  .cancel()
  .then(finalizationResult => /* == { status: "cancelled" } */)
     // ^? RhodiumFinalizedResult<void, never>

[!TIP] Awaiting finalization could be useful, for example, to

  • suspense starting new chains, that use the same non-shareable resource, held by the cancelling chain;
  • check for suppressed cancel errors, which one might want to rethrow;
  • etc.

[!NOTE] The returned Rhodium is the beginning of a new chain. It is detached from the input Rhodium, and will not propagate cancellation to it.

Early cancellation

On cancel, the currently running callback will not be stopped, and it will delay finalization. If the time it takes for a Rhodium to finalize is important, then it might be of interest to optimize this time.

Every callback, attached by then, catch, etc. (except the non-cancellable finally), and the constructor, are provided an AbortSignal as the second argument, which is triggered when that callback has been running at the time of cancel. On signal, the callback should resolve as soon as possible.

Using this signal is completely optional - it is only an optimization.

Example:

const fetchRh = Rhodium.try(
  (url, signal) => fetch(url, { signal })
)

fetchRh.cancel() // Aborts the network request!

Additional methods & syntax sugar

Rhodium.sleep

You no longer have to write the following boilerplate:

new Promise(resolve => setTimeout(resolve, milliseconds))

The same can now be written as Rhodium.sleep(milliseconds), with the advantage of being early cancellable.

[!NOTE] sleep uses AbortSignal.timeout internally!

The timeout is based on active rather than elapsed time, and will effectively be paused if the code is running in a suspended worker, or while the document is in a back-forward cache.

Rhodium.oneSettled

Same as Promise.allSettled(); except the rejection reason is properly typed, and it is applied to one Rhodium instead of an array. Has a non-static shorthand called Rhodium.settled.

A settled Rhodium can be safely awaited! You will not lose the error type, because it makes its way into the resolution type. However, async functions still have rejection type unknown, and cannot be cancelled - for that reason use Rhodium.tryGen.

const myRhodium: Rhodium<"value", Error> = /* ... */
const { value, reason, status } = await myRhodium.settled()
      // ^? value: "value" | undefined
      //    reason: Error | undefined
      //    status: "fulfilled" | "rejected"
if (value) {
	console.log(value, reason, status)
            // ^? value: "value"
            //    reason: undefined
            //    status: "fulfilled"
} else {
	console.log(value, reason, status)
            // ^? value: undefined
            //    reason: Error
            //    status: "rejected"
}

Rhodium.tryGen - the async of Rhodium

Executes a generator function, that is now able to type-safely await Rhodiums, by yield*-ing them instead. When a yielded Rhodium resolves, the generator is resumed with that resolution value.

The return value of the generator becomes the resolution value of tryGen.

[!TIP] The generator function is free to

  • never return;
  • use any JavaScript constructs such as while, for, switch, if, using, etc.;
  • yield* other such generators, including itself;

[!WARNING] But it cannot use await (including await using and for await), because it is not actually an async function.

Rhodium.tryGen(function* () {
  for (let i = 1; i <= 10; i++) {
    const { value: items, reason } = yield* fetchItems(i).settled()
    if (items) {
      console.log(`Page ${i}: ${items}`)
    } else {
      console.error(reason)
    }
    yield* Rhodium.sleep(1000)
  }
})

Rhodium.catchFilter

With the power of type guards, handling specific errors becomes easy. The first argument is a filter for specific errors, and the second is the callback, which gets called with only the allowed errors. Filtered out errors get rejected again, unaffected, essentially "skipping" catchFilter.

const myRhodium: Rhodium<Data, ErrorA | ErrorB> = /* ... */
myRhodium.catchFilter(
  err => err instanceof ErrorA,
  (err /* : ErrorA */) => "handled ErrorA" as const
) // <? Rhodium<Data | "handled ErrorA", ErrorB>

Rhodium.timeout

Attaches a time constraint to a Rhodium. If it fails to settle in the given time, chains after timeout get a rejection, and chain before timeout gets cancelled.

Rhodium
  .sleep(10000)
  .timeout(10) // Uh oh, this rejects, sleep takes too long
  .finalized() // Resolves quickly, because sleep is cancelled!

new Rhodium

There are two ways to use the constructor:

new Rhodium(promiseOrRhodium)

This is identical to Rhodium.resolve, except it always creates a new instance. (When the input value is a Rhodium, resolve simply returns it.)

new Rhodium(executor) - classic Promise constructor

The Rhodium executor has an additional argument, signal. For example, let us convert a pre-Promise-era asynchronous function, that allows cancellation, to a Rhodium:

new Rhodium((resolve, reject, signal) => {
  try {
    longOperation({
      shouldStop: () => signal.aborted,
      onComplete: resolve,
      onError: reject,
    })
  } catch(e) {
    reject(e)
  }
})

[!TIP] If you need to cancel the new Rhodium from inside its executor, you can do so by assigning it to a variable:

const myNewRhodium = new Rhodium((resolve, reject, signal) => {
  /* ... */
  myCancelTrigger.addEventListener('stop',
    () => myNewRhodium.cancel()
  )
})

Just make sure you are not attempting to use the variable before the Rhodium was instantiated.

Error tracking

Rhodium keeps track of all errors a Rhodium chain may reject with, if used correctly.

Rhodium
  .try(
    chance => chance > 0.5
      ? "success"
      : Rhodium.reject("error"),
    Math.random(),
  )
  .then(data => data /* <? "success" */)
  .catch(e => e /* <? "error" */ )
  .then(data => data /* <? "success" | "error" */)

[!CAUTION] This library assumes throw keyword is never used. It is impossible to track types of throw errors. Rhodium has a neverthrow philosophy; you must always use Rhodium.reject() instead. I suggest enforcing this rule if you decide to adopt Rhodium.

[!CAUTION] All errors must be structurally distinct:

  • new SyntaxErrornew TypeError
  • ✔️ class ErrA { code = 1 as const }class ErrB { code = 2 as const }

This is a TypeScript limitation. Any object containing another triggers a subtype reduction. Usually this object would be constructed by Rhodium.reject(), but this does work for everything, e.g., arrays:

class ErrorA extends Error {}
class ErrorB extends Error {}
   // ▼? const result: ErrorA[]
const result = Math.random() > 0.5
  ? [new ErrorA()]
  : [new ErrorB()]

[!IMPORTANT] Other PromiseLike objects returned inside the chain automatically change the error type to unknown. We can never be sure what type they reject, if any. This includes async callbacks, as they always return Promises.

The Errored<T> type

Similarly to Awaited<T>, which returns the resolution type, Errored<T> returns the error type.

const promise = Promise.reject()
type E = Errored<typeof promise>
//   ^? unknown
const promise = Rhodium.resolve()
type E = Errored<typeof promise>
//   ^? never
const promise = Rhodium.reject()
type E = Errored<typeof promise>
//   ^? void
const promise = Rhodium.reject(new TypeError())
type E = Errored<typeof promise>
//   ^? TypeError

Inspired by