Skip to content

tarnishablec/thaterror

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

thaterror πŸ›‘οΈ

npm version Bun Checked codecov GitHub Workflow Status GitHub License bundle size typescript CodeQL Analysis Total TypeScript No Any

A concise, type-safe error handling toolkit for TypeScript inspired by Rust's thiserror. Use the @thaterror/core package to define domain-driven error families with zero boilerplate, then adopt or serialize them with optional adapters (for example, a pino adapter is available).

The Core Value

Handling Error in large-scale TypeScript projects can be frustrating:

  • instanceof is not always reliable across different packages, multiple versions, or due to structural typing matches.
  • Error context (Payload) is often lost during propagation.
  • Integrating third-party errors (e.g., Hono, TypeORM, SyntaxError) into your domain model usually requires messy manual conversion.

thaterror solves these with a Schema-first philosophy, bringing Rust-like ergonomics to TypeScript error handling.

✨ Features

  • 🎯 Zero Boilerplate: A single That call generates error factories with built-in type guards and payload support.
  • πŸ—οΈ Domain-Driven: Define error families that encapsulate your business logic.
  • πŸŒ‰ Native Integration: "Naturalize" external errors into your family using enroll and bridge.
  • 🧠 Intelligent Transformation: The from method provides strict type checking, ensuring only registered error types are processed.
  • 🦾 Total Type Safety: Perfect type narrowing that automatically infers payload types from your schema.
  • πŸ¦€ thiserror-like Experience: Declarative, robust, and designed for developers who value type correctness.

πŸš€ Quick Start

This repository is split into focused packages:

Installation

To use the core library:

bun add @thaterror/core
// or with npm
npm install @thaterror/core

If you want the pino adapter for structured logging:

bun add @thaterror/pino-adapter pino

See the individual package READMEs for development and testing instructions.

Comparison ✨ β€” Rust thiserror / @thaterror/core / Native Error

Rust (thiserror) πŸ¦€

use thiserror::Error;
// Add serde derives for easy serialization/deserialization
use serde::{Serialize, Deserialize};

#[derive(Debug, Error, Serialize, Deserialize)]
pub enum AppError {
    #[error("not found: {0}")]
    NotFound(String),

    #[error("invalid input: {0}")]
    InvalidInput(String),

    // Use a serializable payload in the README example to keep the serde example accurate.
    #[error("db error: {0}")]
    DbError(String),
}

// Quick notes for thiserror:
// - Type & payload: enums carry payloads at the type level and enable exhaustive matching.
// - Pattern matching: use `match` for exhaustive, compile-time checked branching.
// - Serialization: straightforward with `serde` β€” derive `Serialize`/`Deserialize`. For a clean
//   discriminated JSON shape, add a serde tag on the enum.
// - Cost: language-level safety and performance; highly reliable for in-binary error modeling.

TypeScript (@thaterror/core) πŸ›‘οΈ

import {That, type ThatError} from '@thaterror/core';

const AppError = That({
    NotFound: (id: string) => `not found: ${id}`,
    InvalidInput: (input: string) => `invalid input: ${input}`,
    DbError: (query: string) => `db error: ${query}`,
});

type AppErrorType = ThatError<typeof AppError>;

throw AppError.NotFound('123');

// Quick notes for @thaterror/core:
// - Type & payload: carries payloads in the type system and supports safe narrowing.
// - Pattern matching: built-in type guards enable exhaustive, type-safe branching.
// - Cross-package reliability: guards and schema-style definitions are robust across modules.
// - Serialization & adapters: ships adapters (e.g. pino) for structured logging/transport.
// - Cost: minimal boilerplate with strong TypeScript typing.

Use @thaterror/core with neverthrow β€” full Rust-like Result<T, E> experience πŸ¦€

// Combine @thaterror/core (typed errors) with neverthrow's Result for ergonomic,
// exhaustiveness-friendly flow control similar to Rust's `Result<T, E>`.
import {type ThatError} from '@thaterror/core';
import {errAsync, ResultAsync} from 'neverthrow';
import {AppError} from './error'
import {dbFind} from 'db'; // assume dbFind(id) returns Promise<User | undefined>

type AppErrorType = ThatError<typeof AppError>;

// Async neverthrow example function to find a user by ID:
// - returns ResultAsync<User, AppError>
const findUser = (id: string): ResultAsync<T, AppErrorType> => {
    // Immediate validation using neverthrow's async helpers (no try/catch)
    if (!id) return errAsync(AppError.InvalidInput(id));

    // Wrap the DB promise and map any rejection into AppError
    return ResultAsync.fromPromise(
        dbFind(id),
        // asume e.query is string
        (e) => AppError.DbError(e.query).with({cause: e})
    ).andThen(r => {
        if (!r) return errAsync(AppError.NotFound(id));
        return ResultAsync.ok(r);
    })
};

TypeScript (native Error) ⚠️

class NotFoundError extends Error {
  constructor(public id: string) {
    super(`not found: ${id}`);
    this.name = 'NotFoundError';
  }
}
class InvalidInputError extends Error {
  constructor(public input: string) {
    super(`invalid input: ${input}`);
    this.name = 'InvalidInputError';
  }
}
class DbError extends Error {
  constructor(public query: string) {
    super('db error');
    this.name = 'DbError';
  }
}

throw new NotFoundError('123')

// Quick notes for native Error:
// - Lack of scope: no built-in grouping of related errors.
// - Type & payload: requires manual extension; TypeScript typing is weaker compared to enums/That.
// - Pattern matching: consumers often use `instanceof` or `error.name`, which is less safe.
// - Cross-package reliability: `instanceof` can be fragile across bundles/versions.
// - Serialization: needs explicit conversion for structured logs/transport.
// - Cost: simplest to implement but lacks the type-level guarantees of the other approaches.

πŸ“œ License

MPL-2.0