@livekit/throws-transformer
v0.1.5
Published
TypeScript transformer that enforces error handling via branded Throws<T, E> types
Maintainers
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:
- Catch the declared errors, or
- 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 typescriptVS 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
- Open any
.tsfile - Click the TypeScript version in the bottom-right status bar (e.g., "TypeScript 5.0.0")
- 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 installAdd 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/*.tsUsage
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:
NetworkErrorNotFoundErrorValidationErrorParseError
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
- Make sure you selected "Use Workspace Version" for TypeScript
- Run
TypeScript: Restart TS Server - 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.jsRebuild if necessary:
cd node_modules/@livekit/throws-transformer && npm run buildLimitations
- Third-party libraries: Only works with functions that use
Throws<>annotations - Dynamic throws: Static analysis only - can't detect runtime-conditional throws
- VS Code only: The language service plugin is VS Code specific (other editors may vary)
License
Apache 2.0
