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

@unthrown/oxlint

v5.12.0

Published

oxlint plugin enforcing unthrown's conventions

Readme

@unthrown/oxlint

An oxlint plugin that enforces unthrown's conventions.

📖 Documentation

pnpm add -D @unthrown/oxlint oxlint

A small set of lint rules that keep unthrown code honest — turning the library's theses into automated checks.

Rules

| Rule | What it enforces | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | unthrown/no-ambiguous-error-type | The E in Result<T, E> / AsyncResult<T, E> must name a concrete domain error — no unknown, any, Error, object, bare {}, void, or primitives. (never is allowed.) This is Thesis #1: E is only the anticipated failures. Covers the matcher's returnType<R>() pin too, but only in mapErrCases, where the pin is the new E. | | unthrown/prefer-async-result | Prefer AsyncResult<T, E> over Promise<Result<T, E>> — a raw Promise<Result> can still reject. Autofixable, and the fix adds the AsyncResult specifier to an existing unthrown import when the name is not already in scope. Withheld on an async function's own return annotation and on a function type's return position (the implementer may be async, so the rewrite would not compile), when the name AsyncResult is already bound to something else, and when there is no specifier list to extend (a namespace import). | | unthrown/no-unhandled-result | A Result / AsyncResult returned by a bare call (awaited or not) must not be dropped on the floor — it carries the error channel; dropping it silently discards failures. Bind it, return it, or eliminate it with match / a get* extractor. Syntactic: it sees unthrown's producers and functions annotated Result/AsyncResult in the same file; it misses a result returned by a function imported from another of your modules, and a dropped method chain (r.map(f);). No type-aware rule backs it up — typescript-eslint's no-floating-promises ignores AsyncResult, whose then has no rejection callback. | | unthrown/no-async-result-race | No sibling AsyncResult construction while an earlier one is still unconsumed. An AsyncResult is eager — constructing it starts the work — so the readable spelling of a sequence, each step in its own const and then chained, is a silent race: it type-checks, it returns a Result, and it runs the steps concurrently. Sequence with flatTap (a later step needs only the earlier one's success) or DoAsync().bind(...) (it needs the value). | | unthrown/no-catch-all-pattern | No P._ catch-all in a matcher (nor ts-pattern's P.any alias, nor the empty object pattern .with({}, …), which matches every object) — enumerate every error case by name (.with(P.tag("A"), P.tag("B"), …, handler), grouping cases that share a handler), so a new error can't be silently absorbed. This is unthrown's default position; P._ is an escape hatch — a helper generic in E, or an E that is a single type rather than a union — and carries a targeted oxlint-disable. See below. | | unthrown/no-unused-matcher | A …Cases callback (the five error combinators, and match's errCases handler) must use the matcher it was handed. The injected matcher is the only builder bound to the actual error — a builder sourced elsewhere satisfies the structural ExhaustiveMatch constraint but picks its branch from whatever value it closed over: the wrong case is recovered silently, or nothing matches and the modeled error becomes a Defect. Also flags a second match(...) built inside the callback's own body (branch handlers are free to match their payload). | | unthrown/no-throw | Opt-in (not in recommended): no raw throw statements — errors are returned (Err(...)), only a true defect ever throws. A modeled failure → return Err(...); a failure genuinely unmodeled here → recoverErrCases + get (routing the case to the injected defect(...)); a known-technical precondition throw → a plain helper wrapped once with fromSafeThrowable; a deliberate throw site carries a targeted oxlint-disable. oxlint ships no no-restricted-syntax, so this rule is the only way to enforce the ban. | | unthrown/prefer-pre-lifted | Opt-in (not in recommended): no .toAsync() on a freshly constructed Ok(...) / Err(...) — OkAsync(value) and ErrAsync(error) are what unthrown ships for that, and the fresh literal is built only to be thrown away. The receiver is the whole test, which is what makes it safe where the removed prefer-ensure was not: .toAsync() on a Result that already exists (a variable, a call's return, a ternary, fromNullable(...)) is the combinator doing its job and is never reported. Autofixable — the pre-lifted name with the arguments untouched, Ok() and Ok(undefined) collapsing to OkAsync(), and the specifier added to the existing unthrown import. | | unthrown/no-get-or-throw | Opt-in (not in recommended): no getOrThrow() — it throws the modeled error, abandoning errors-as-values at the last step. Fold the error channel instead: recoverErrCases((matcher, defect) => …) empties E, so .get() compiles. Matched as a zero-argument member call, so Effect's one-argument Option.getOrThrow(o) is left alone. getOrThrow() is right in a test — exempt those files with an oxlint overrides entry rather than a rule option. Pairs with no-throw: with both on, there is no escape left. |

Most rules resolve import bindings via scope analysis (through the imported name, so renamed imports resolve too), and only fire on unthrown's own Result / AsyncResult — a Result from another library is left alone. A few are keyed on a name or shape instead — unthrown's own vocabulary, needing no Result binding to resolve: no-get-or-throw (a zero-argument .getOrThrow() member call), no-unused-matcher (the …Cases method names), and the returnType<R>() pin on a mapErrCases callback's own matcher parameter. no-throw is keyed on the language statement itself — it reports every throw, with nothing to resolve at all. prefer-pre-lifted resolves the constructor through scope like the first group, but keys on the receiver rather than a type annotation.

Usage

Register the plugin and enable its rules in your .oxlintrc.json:

{
  "jsPlugins": [{ "name": "unthrown", "specifier": "@unthrown/oxlint" }],
  "rules": {
    "unthrown/no-ambiguous-error-type": "error",
    "unthrown/no-catch-all-pattern": "error",
    "unthrown/no-get-or-throw": "error",
    "unthrown/no-unhandled-result": "error",
    "unthrown/no-throw": "error",
    "unthrown/no-unused-matcher": "error",
    "unthrown/prefer-async-result": "error"
  },
  "overrides": [
    {
      "files": ["**/*.test.ts", "**/*.spec.ts"],
      "rules": { "unthrown/no-get-or-throw": "off" }
    }
  ]
}

The package's default export also exposes a recommended preset (an oxlint config that registers the plugin and turns the recommended rules on — no-throw and no-get-or-throw are the two explicit opt-ins) for setups that build their config programmatically:

import unthrown from "@unthrown/oxlint";
// unthrown.recommended → { jsPlugins: [...], rules: { "unthrown/...": "error" } }

oxlint is a peer dependency. JS plugins require a recent oxlint (≥ 1.69).

ESLint

The plugin is also a regular ESLint plugin (its rules are wrapped with @oxlint/plugins' eslintCompatPlugin). Register it in a flat config with the typescript-eslint parser — the rules read type annotations, but need no type information:

// eslint.config.js
import unthrown from "@unthrown/oxlint";
import tseslint from "typescript-eslint";

export default [
  {
    files: ["**/*.ts"],
    languageOptions: { parser: tseslint.parser },
    plugins: { unthrown },
    rules: {
      "unthrown/no-ambiguous-error-type": "error",
      "unthrown/no-async-result-race": "error",
      "unthrown/no-catch-all-pattern": "error",
      "unthrown/no-unhandled-result": "error",
      "unthrown/no-unused-matcher": "error",
      "unthrown/prefer-async-result": "error",
    },
  },
];

unthrown.recommended is an oxlint config, not an ESLint one — list the rules yourself as above.

The catch-all escape hatch

no-catch-all-pattern is on by default, so P._ needs a reason wherever it survives. Two cases are legitimate. The first is a helper generic in E: no list of arms can prove exhaustiveness against an unresolved type parameter — only the catch-all can, because it is a state transition to "nothing remains" rather than a subtraction from E. The second is an E that is a single type rather than a union of cases — a validator's issues array, say — where one arm is the enumeration. Keep the catch-all there, and say which it is:

import { P, type Result } from "unthrown";

const toApiError = <T, E>(result: Result<T, E>): Result<T, ApiError> =>
  result.mapErrCases((matcher) =>
    // oxlint-disable-next-line unthrown/no-catch-all-pattern -- generic in `E`: no arm list can prove exhaustiveness
    matcher
      .returnType<ApiError>()
      .with(P._, (error) => new ApiError({ status: 500, error })),
  );

Everywhere the error union is concrete, name the cases instead — grouping the ones that share a handler:

import { P } from "unthrown";

// E is `NotFound | Conflict | DriverError` — every member gets an arm:
result.mapErrCases((matcher) =>
  matcher
    .with(P.tag("NotFound"), () => new ApiError({ status: 404 }))
    .with(
      P.tag("Conflict"),
      P.tag("DriverError"),
      (e) => new ApiError({ status: 500, error: e }),
    ),
);

License

MIT © Benoit TRAVERS