
Company News
Socket Named Top Sales Organization by RepVue
Socket won two 2026 Reppy Awards from RepVue, ranking in the top 5% of all sales orgs. AE Alexandra Lister shares what it's like to grow a sales career here.
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.
PromisePromiseRhodium 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()Promise. Simply get the promise property of a Rhodium instance.[!NOTE] Conversion to
Promiseloses cancelability and otherRhodium-exclusive features.
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
throwkeyword is never used. It is impossible to track types ofthrowerrors.Rhodiumhas a neverthrow philosophy; you must always useRhodium.reject()instead. I suggest enforcing this rule if you decide to adoptRhodium.
[!CAUTION] All errors must be structurally distinct:
- ❌
new SyntaxError≈new 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
PromiseLikeobjects returned inside the chain automatically change the error type tounknown. We can never be sure what type they reject, if any. This includesasynccallbacks, as they always returnPromises.
Errored<T> typeSimilarly 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
Cancellation prevents any further callbacks from running.
[!IMPORTANT]
finallyis completely unaffected by the cancellation feature. This callback will always execute, and it can be attached to a cancelledRhodium.
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]
cancelreturns aRhodium, but it is actually synchronous at its core. Oncecancelis run, its effects are immediate.- If
Rhodiumrejects right before cancellation, the reason might get supressed. IfRhodiumrejects 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.
[!WARNING]
- Only the last
Rhodiumin 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 usingcancelledproperty is possible, if that is the intended behaviour.
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:
D gets cancelled, so does C.G gets cancelled, so do F and then E.D and G get cancelled, no matter the order, do B and A get cancelled as well.Rhodium.oneFinalizedAlso 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, orfinally 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 supressed
cancelerrors, which one might want to rethrow;- etc.
[!NOTE] The returned
Rhodiumis the beginning of a new chain. It is detached from the inputRhodium, and will not propagate cancellation to it.
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) is 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.
Rhodium.sleepYou 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]
sleepusesAbortSignal.timeoutinternally!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.oneSettledSame 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 RhodiumExecutes 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(includingawait usingandfor await), because it is not actually anasyncfunction.
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.catchFilterWith 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.timeoutAttaches 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!
FAQs
A TypeScript `Promise` wrapper that adds syntax sugar.
The npm package rhodium receives a total of 5 weekly downloads. As such, rhodium popularity was classified as not popular.
We found that rhodium demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Socket won two 2026 Reppy Awards from RepVue, ranking in the top 5% of all sales orgs. AE Alexandra Lister shares what it's like to grow a sales career here.

Security News
NIST will stop enriching most CVEs under a new risk-based model, narrowing the NVD's scope as vulnerability submissions continue to surge.

Company News
/Security News
Socket is an initial recipient of OpenAI's Cybersecurity Grant Program, which commits $10M in API credits to defenders securing open source software.