within-ts

Tagged Errors

One-line error classes with discriminated unions

The simplest building block — a concise way to define Error classes with discriminated unions.

The problem

Standard try/catch in TypeScript provides no type information about the error:

try {
  await getUser("123")
} catch (e) {
  // `e` is `unknown`. No type narrowing. No way to know what went wrong.
}

Custom error classes solve this, but require significant boilerplate:

class NotFoundError extends Error {
  readonly name = "NotFoundError"
  readonly id: string
  constructor(id: string) {
    super(`Not found: ${id}`)
    this.id = id
  }
}

Every error class needs the same boilerplate: extend Error, set name, declare fields, write a constructor, call super. Multiply this by every error in your app.

The solution

Define.Error generates these classes for you in one line:

import { Define } from "within-ts"

class NotFoundError extends Define.Error("NotFound")<{
  readonly id: string
}>() {}

class ValidationError extends Define.Error("Validation")<{
  readonly field: string
  readonly message: string
}>() {}

That's it. Each error class:

  • Extends Error (so it has .message, .stack, etc.)
  • Has a _tag property with a literal string type ("NotFound", "Validation")
  • Takes its fields as a single object in the constructor
  • Fields are assigned directly onto the instance

Usage:

const err = new NotFoundError({ id: "123" })

err._tag           // "NotFound"  (literal type, not just string)
err.id             // "123"
err instanceof Error         // true
err instanceof NotFoundError // true

Why _tag?

The _tag field enables discriminated unions. Use a switch to narrow the type:

function handle(err: NotFoundError | ValidationError) {
  switch (err._tag) {
    case "NotFound":
      // TypeScript knows `err` is NotFoundError here
      console.log(`Missing: ${err.id}`)
      break
    case "Validation":
      // TypeScript knows `err` is ValidationError here
      console.log(`Bad field: ${err.field}`)
      break
  }
}

This is called a discriminated union — TypeScript narrows the type based on _tag. No instanceof chains, no type guards. Just a switch on a string.

On this page