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

@livekit/throws-transformer

v0.1.5

Published

TypeScript transformer that enforces error handling via branded Throws<T, E> types

Readme

throws-transformer

A TypeScript transformer that enforces error handling via branded Throws<T, E> types.

The Problem

TypeScript doesn't have checked exceptions. When you call a function, you have no compile-time knowledge of what errors it might throw:

function parseJSON(input: string): User {
  return JSON.parse(input); // Can throw SyntaxError - nothing tells you!
}

The Solution

This transformer introduces a Throws<T, E> branded type that encodes possible errors in the return type:

function parseJSON(input: string): Throws<User, SyntaxError> {
  // ...
}

The transformer then enforces that callers either:

  1. Catch the declared errors, or
  2. Propagate them by declaring them in their own return type

The Throws<T, E> type is intended to be an internal implementation detail — your public API surfaces normal return types, while Throws<> annotations are used internally to get compile-time safety for error handling within your codebase. See Pattern 4: Public API Boundary for how to strip Throws<> at the edge of your public API.

The Throws<T, E> type is entirely opt in - ie, if a function doesn't return a branded type or call functions within that return a branded type, the check will pass. This makes gradual migration possible.

If you need to throw inside a Throws-annotated function without declaring the error in the branded type (for example, assertion-style "panic" errors), add a // @throws-transformer ignore comment above the throw and the checker will skip it:

function parseJSON(input: string): Throws<User, SyntaxError> {
  if (input.length === 0) {
    // @throws-transformer ignore
    throw new Error('Assertion failed: input was empty');
  }

  // ...
}

The // @throws-transformer ignore comment can also include an optional reason for documentation:

// @throws-transformer ignore - assertion errors should panic
throw new Error('Unchecked error');

Installation

npm install @livekit/throws-transformer typescript

VS Code Setup (Recommended)

To get real-time error squiggles in VS Code:

Step 1: Configure tsconfig.json

{
  "compilerOptions": {
    "plugins": [{ "name": "@livekit/throws-transformer" }]
  }
}

Step 2: Configure VS Code to use workspace TypeScript

Create or update .vscode/settings.json:

{
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}

Step 3: Select TypeScript version

  1. Open any .ts file
  2. Click the TypeScript version in the bottom-right status bar (e.g., "TypeScript 5.0.0")
  3. Select "Use Workspace Version"

Or run the command: TypeScript: Select TypeScript Version...

Step 4: Restart the TypeScript server

Run command: TypeScript: Restart TS Server

You should now see red squiggles for unhandled Throws errors!

Build-time Errors with ts-patch

For errors during tsc compilation (CI, build scripts):

npm install ts-patch
npx ts-patch install

Add the transformer to tsconfig.json:

{
  "compilerOptions": {
    "plugins": [
      { "name": "@livekit/throws-transformer" },
      {
        "name": "@livekit/throws-transformer/transformer",
        "transform": "@livekit/throws-transformer/transformer"
      }
    ]
  }
}

Now tsc will emit errors for unhandled throws.

CLI Checker

For quick checks without modifying your build:

# Check specific files
npx --package @livekit/throws-transformer throws-check src/myfile.ts

# Check multiple files
npx --package @livekit/throws-transformer throws-check src/*.ts

Usage

1. Define your functions with Throws<T, E>

import { Throws } from '@livekit/throws-transformer/throws';

export class NetworkError extends Error {
  constructor(message: string = 'Network request failed') {
    super(message);
    this.name = 'NetworkError';
  }
}

export class NotFoundError extends Error {
  constructor(message: string = 'Resource not found') {
    super(message);
    this.name = 'NotFoundError';
  }
}

function fetchUser(id: string): Throws<User, NetworkError | NotFoundError> {
  if (!id) {
    throw new NotFoundError();
  }
  // ... fetch logic that might throw NetworkError
  return user;
}

2. Handle or propagate the errors

The checker will report unhandled errors:

Unhandled error(s) from 'fetchUser': NetworkError | NotFoundError.
Catch these errors or add 'Throws<..., NetworkError | NotFoundError>' to your function's return type.

Handling Patterns

Pattern 1: Catch and Handle

function getUserName(id: string): string | null {
  try {
    const user = fetchUser(id);
    return user.name;
  } catch (e) {
    if (e instanceof NetworkError) {
      console.error('Network failed');
      return null;
    }
    if (e instanceof NotFoundError) {
      console.error('User not found');
      return null;
    }
    throw e; // Re-throw unknown errors
  }
}

Pattern 2: Propagate in Return Type

function fetchAndValidate(
  id: string,
): Throws<ValidatedUser, NetworkError | NotFoundError | ValidationError> {
  const user = fetchUser(id); // NetworkError | NotFoundError propagated
  const validated = validateUser(user); // ValidationError propagated
  return validated;
}

Pattern 3: Partial Handling

function fetchWithFallback(id: string): Throws<User, NetworkError> {
  try {
    return fetchUser(id);
  } catch (e) {
    if (e instanceof NotFoundError) {
      return getDefaultUser(); // Handle NotFoundError locally
    }
    throw e; // NetworkError is propagated (declared in return type)
  }
}

Pattern 4: Public API Boundary

Since Throws<T, E> is meant to be an internal implementation detail, you'll want to strip it at the boundary of your public API. Use a catch-and-rethrow pattern — because the caught error e is typed as unknown, the transformer won't require you to declare it:

// Internal function with Throws annotation
function internalFetchUser(id: string): Throws<User, NetworkError | NotFoundError> {
  if (!id) {
    throw new NotFoundError();
  }
  // ... fetch logic
  return user;
}

// Public API — clean return type, no Throws<> leaking out
export function getUser(id: string): User {
  try {
    return internalFetchUser(id);
  } catch (e) {
    throw e; // Re-throw as unknown — Throws<> is stripped at this boundary
  }
}

Pattern 5: Structured Error Types

For richer error handling, you can define structured error types with reason codes, similar to Rust's thiserror crate. This pairs well with Throws<> to give callers both compile-time safety and runtime introspection:

abstract class ReasonedError<Reason> extends Error {
  abstract readonly reason: Reason;

  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
  cause?: unknown;

  constructor(message?: string, options?: { cause?: unknown }) {
    super(message || 'an error has occurred');
    if (typeof options?.cause !== 'undefined') {
      this.cause = options.cause;
    }
  }
}

enum PaymentErrorReason {
  InsufficientFunds = 0,
  CardDeclined = 1,
  NetworkFailure = 2,
}

class PaymentError<R extends PaymentErrorReason = PaymentErrorReason> extends ReasonedError<R> {
  readonly name = 'PaymentError';
  readonly reason: R;
  readonly reasonName: string;

  constructor(message: string, reason: R, options?: { cause?: unknown }) {
    super(message, options);
    this.reason = reason;
    this.reasonName = PaymentErrorReason[reason];
  }

  static insufficientFunds() {
    return new PaymentError('Insufficient funds', PaymentErrorReason.InsufficientFunds);
  }

  static cardDeclined() {
    return new PaymentError('Card declined', PaymentErrorReason.CardDeclined);
  }

  static networkFailure(cause?: unknown) {
    return new PaymentError('Network failure', PaymentErrorReason.NetworkFailure, {
      cause,
    });
  }
}

// Use with Throws<> — callers must handle PaymentError
function processPayment(amount: number): Throws<Receipt, PaymentError> {
  if (amount > getBalance()) {
    throw PaymentError.insufficientFunds();
  }

  let result;
  try {
    result = chargeCard(amount);
  } catch (error) {
    throw PaymentError.networkFailure(error);
  }

  return result;
}

// You can also narrow to specific reasons:
function smallPayment(
  amount: number,
): Throws<Receipt, PaymentError<PaymentErrorReason.CardDeclined>> {
  // ...
}

Async Functions

Works with Promise<Throws<T, E>>:

async function fetchUserAsync(id: string): Promise<Throws<User, NetworkError>> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new NetworkError();
  return res.json();
}

// ❌ Error: Unhandled NetworkError
async function getName(id: string): Promise<string> {
  const user = await fetchUserAsync(id);
  return user.name;
}

// ✅ OK: Error is handled
async function getNameSafe(id: string): Promise<string | null> {
  try {
    const user = await fetchUserAsync(id);
    return user.name;
  } catch (e) {
    if (e instanceof NetworkError) return null;
    throw e;
  }
}

API

Types

// Brand a return type with possible errors
type Throws<T, E extends Error = never> = T & { readonly __throws?: E };

// Extract error types from a Throws type
type ExtractErrors<T> = T extends Throws<any, infer E> ? E : never;

// Extract success type from a Throws type
type ExtractSuccess<T> = T extends Throws<infer S, any> ? S : T;

Built-in Error Classes

The package includes some common error classes:

  • NetworkError
  • NotFoundError
  • ValidationError
  • ParseError

You can also define your own:

class DatabaseError extends Error {
  constructor(message = 'Database operation failed') {
    super(message);
    this.name = 'DatabaseError';
  }
}

Troubleshooting

Errors not showing in VS Code

  1. Make sure you selected "Use Workspace Version" for TypeScript
  2. Run TypeScript: Restart TS Server
  3. Check the TypeScript output panel for plugin initialization messages

Plugin not loading

Verify the plugin is installed in node_modules:

ls node_modules/@livekit/throws-transformer/dist/plugin.js

Rebuild if necessary:

cd node_modules/@livekit/throws-transformer && npm run build

Limitations

  1. Third-party libraries: Only works with functions that use Throws<> annotations
  2. Dynamic throws: Static analysis only - can't detect runtime-conditional throws
  3. VS Code only: The language service plugin is VS Code specific (other editors may vary)

License

Apache 2.0