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

errore

v0.10.0

Published

Type-safe errors as values for TypeScript. Like Go, but with full type inference.

Readme

errore

Type-safe errors as values for TypeScript. Like Go, but with full type inference.

Why?

Instead of wrapping values in a Result<T, E> type, functions simply return E | T. TypeScript's type narrowing handles the rest:

// Go-style: errors as values
const user = await getUser(id)
if (user instanceof NotFoundError) {
  console.error('Missing:', user.id)
  return
}
if (user instanceof DbError) {
  console.error('DB failed:', user.reason)
  return
}
console.log(user.username)  // user is User, fully narrowed

Install

npm install errore

Quick Start

Define typed errors with variable interpolation and return Error or Value directly:

import * as errore from 'errore'

// Define typed errors with $variable interpolation
class NotFoundError extends errore.createTaggedError({
  name: 'NotFoundError',
  message: 'User $id not found'
}) {}

class DbError extends errore.createTaggedError({
  name: 'DbError',
  message: 'Database query failed: $reason'
}) {}

// Function returns Error | Value (no wrapper!)
async function getUser(id: string): Promise<NotFoundError | DbError | User> {
  const result = await errore.tryAsync({
    try: () => db.query(id),
    catch: e => new DbError({ reason: e.message, cause: e })
  })
  
  if (result instanceof Error) return result
  if (!result) return new NotFoundError({ id })
  
  return result
}

// Caller handles errors explicitly
const user = await getUser('123')

if (user instanceof Error) {
  const message = errore.matchError(user, {
    NotFoundError: e => `User ${e.id} not found`,
    DbError: e => `Database error: ${e.reason}`,
    Error: e => `Unexpected error: ${e.message}`
  })
  console.log(message)
  return
}

// TypeScript knows: user is User
console.log(user.name)

Example: API Error Handling

A complete example with custom base class and HTTP status codes:

import * as errore from 'errore'

// Base class with shared functionality
class AppError extends Error {
  statusCode: number = 500
  
  toResponse() {
    return { error: this.message, code: this.statusCode }
  }
}

// Specific errors with status codes and $variable interpolation
class NotFoundError extends errore.createTaggedError({
  name: 'NotFoundError',
  message: '$resource not found',
  extends: AppError
}) {}

class ValidationError extends errore.createTaggedError({
  name: 'ValidationError',
  message: 'Invalid $field: $reason',
  extends: AppError
}) {}

class UnauthorizedError extends errore.createTaggedError({
  name: 'UnauthorizedError',
  message: '$message',
  extends: AppError
}) {}

// Service function
async function updateUser(
  userId: string,
  data: { email?: string }
): Promise<NotFoundError | ValidationError | UnauthorizedError | User> {
  const session = await getSession()
  if (!session) {
    return new UnauthorizedError({ message: 'Not logged in' })
  }
  
  const user = await db.users.find(userId)
  if (!user) {
    return new NotFoundError({ resource: `User ${userId}` })
  }
  
  if (data.email && !isValidEmail(data.email)) {
    return new ValidationError({ field: 'email', reason: 'Invalid email format' })
  }
  
  return db.users.update(userId, data)
}

// API handler
app.post('/users/:id', async (req, res) => {
  const result = await updateUser(req.params.id, req.body)
  
  if (result instanceof Error) {
    // All errors have toResponse() from AppError base
    return res.status(result.statusCode).json(result.toResponse())
  }
  
  return res.json(result)
})

API

createTaggedError

Create typed errors with variable interpolation in the message:

import * as errore from 'errore'

// Variables are extracted from the message and required in constructor
class NotFoundError extends errore.createTaggedError({
  name: 'NotFoundError',
  message: 'User $id not found in $database'
}) {}

const err = new NotFoundError({ id: '123', database: 'users' })
err.message   // 'User 123 not found in users'
err.id        // '123'
err.database  // 'users'
err._tag      // 'NotFoundError'

// Error without variables
class EmptyError extends errore.createTaggedError({
  name: 'EmptyError',
  message: 'Something went wrong'
}) {}
new EmptyError()  // no args required

// With cause for error chaining
class WrapperError extends errore.createTaggedError({
  name: 'WrapperError',
  message: 'Failed to process $item'
}) {}
new WrapperError({ item: 'data', cause: originalError })

// With custom base class
class AppError extends Error {
  statusCode = 500
}

class HttpError extends errore.createTaggedError({
  name: 'HttpError',
  message: 'HTTP $status error',
  extends: AppError
}) {}

const err = new HttpError({ status: 404 })
err.statusCode  // 500 (inherited from AppError)
err instanceof AppError  // true

Error Wrapping and Context

Wrap errors with additional context while preserving the original error via cause:

// Wrap with context, preserve original in cause
async function processUser(id: string): Promise<ServiceError | ProcessedUser> {
  const user = await getUser(id)  // returns NotFoundError | User
  
  if (user instanceof Error) {
    return new ServiceError({ id, cause: user })
  }
  
  return process(user)
}

// Access original error via cause
const result = await processUser('123')
if (result instanceof Error) {
  console.log(result.message)  // "Failed to process user 123"
  
  if (result.cause instanceof NotFoundError) {
    console.log(result.cause.id)  // access original error's properties
  }
}

The error definitions:

class NotFoundError extends errore.createTaggedError({
  name: 'NotFoundError',
  message: 'User $id not found'
}) {}

class ServiceError extends errore.createTaggedError({
  name: 'ServiceError',
  message: 'Failed to process user $id'
}) {}

Browser console prints the full cause chain:

ServiceError: Failed to process user 123
    at processUser (app.js:12)
    at main (app.js:20)
Caused by: NotFoundError: User 123 not found
    at getUser (app.js:5)
    at processUser (app.js:8)

findCause

Walk the .cause chain to find an ancestor matching a specific error class. Similar to Go's errors.As — checks the error itself first, then traverses .cause recursively:

import * as errore from 'errore'

class NotFoundError extends errore.createTaggedError({
  name: 'NotFoundError',
  message: 'User $id not found'
}) {}

class ServiceError extends errore.createTaggedError({
  name: 'ServiceError',
  message: 'Failed to process user $id'
}) {}

// Deep chain: ServiceError -> NotFoundError
const notFound = new NotFoundError({ id: '123' })
const service = new ServiceError({ id: '123', cause: notFound })

// Instance method on tagged errors
const found = service.findCause(NotFoundError)
found?.id  // '123' — type-safe access

// Standalone function for any Error
const found2 = errore.findCause(service, NotFoundError)
found2?.id  // '123'

This solves the problem where result.cause instanceof MyError only checks one level deep. findCause walks the entire chain:

// A -> B -> C chain
const c = new DbError({ message: 'connection reset' })
const b = new ServiceError({ id: '123', cause: c })
const a = new ApiError({ message: 'request failed', cause: b })

// Manual check only finds B
a.cause instanceof DbError  // false — only checks one level

// findCause walks the full chain
a.findCause(DbError)  // finds C ✓

Returns undefined if no matching ancestor is found. Safe against circular .cause references.

Custom Base Class with extends

Use extends to inherit from a custom base class. The error will pass instanceof for both the base class and the specific error class:

class AppError extends Error {
  statusCode = 500
  toResponse() { return { error: this.message, code: this.statusCode } }
}

class NotFoundError extends errore.createTaggedError({
  name: 'NotFoundError',
  message: 'Resource $id not found',
  extends: AppError
}) {
  statusCode = 404
}

const err = new NotFoundError({ id: '123' })
err instanceof NotFoundError  // true
err instanceof AppError       // true
err instanceof Error          // true

err.statusCode    // 404
err.toResponse()  // { error: 'Resource 123 not found', code: 404 }

Type Guards

Use instanceof checks to narrow union types:

const result: NetworkError | User = await fetchUser(id)

if (result instanceof Error) {
  // result is NetworkError
  return result
}
// result is User

Try Functions

Wrap exceptions as error values:

import * as errore from 'errore'

// Sync - wraps exceptions in UnhandledError
const parsed = errore.try(() => JSON.parse(input))

// Sync - with custom error type
const parsed = errore.try({
  try: () => JSON.parse(input),
  catch: e => new ParseError({ reason: e.message, cause: e })
})

// Async
const response = await errore.tryAsync(() => fetch(url))

// Async - with custom error
const response = await errore.tryAsync({
  try: () => fetch(url),
  catch: e => new NetworkError({ url, cause: e })
})

Transformations

Transform and chain operations:

import * as errore from 'errore'

// Transform value (if not error)
const name = errore.map(user, u => u.name)

// Transform error
const appError = errore.mapError(dbError, e => new AppError({ cause: e }))

// Chain operations
const posts = errore.andThen(user, u => fetchPosts(u.id))

// Side effects
const logged = errore.tap(user, u => console.log('Got user:', u.name))

Extraction

Extract values or throw, split arrays by success/error:

import * as errore from 'errore'

// Extract or throw
const user = errore.unwrap(result)
const user = errore.unwrap(result, 'Custom error message')

// Extract or fallback
const name = errore.unwrapOr(result, 'Anonymous')

// Pattern match
const message = errore.match(result, {
  ok: user => `Hello, ${user.name}`,
  err: error => `Failed: ${error.message}`
})

// Split array into [successes, errors]
const [users, errors] = errore.partition(results)

Error Matching

Exhaustive pattern matching with matchError. Always assign results to a variable and keep callbacks pure:

import * as errore from 'errore'

class ValidationError extends errore.createTaggedError({
  name: 'ValidationError',
  message: 'Invalid $field'
}) {}

class NetworkError extends errore.createTaggedError({
  name: 'NetworkError',
  message: 'Failed to fetch $url'
}) {}

// Exhaustive matching - Error handler is always required
const message = errore.matchError(error, {
  ValidationError: e => `Invalid ${e.field}`,
  NetworkError: e => `Failed to fetch ${e.url}`,
  Error: e => `Unexpected: ${e.message}`  // required fallback for plain Error
})
console.log(message)  // side effects outside callbacks

// Partial matching with fallback
const fallbackMsg = errore.matchErrorPartial(error, {
  ValidationError: e => `Invalid ${e.field}`
}, e => `Unknown error: ${e.message}`)

// Type guards
ValidationError.is(value)  // specific class

How Type Safety Works

TypeScript narrows types after instanceof Error checks:

function example(result: NetworkError | User): string {
  if (result instanceof Error) {
    // TypeScript knows: result is NetworkError
    return result.message
  }
  // TypeScript knows: result is User (Error excluded)
  return result.name
}

This works because:

  1. Error is a built-in class TypeScript understands
  2. Custom error classes extend Error
  3. After an instanceof Error check, TS excludes all Error subtypes

Result + Option Combined: Error | T | null

Naturally combine error handling with optional values. No wrapper nesting needed!

import * as errore from 'errore'

class NotFoundError extends errore.createTaggedError({
  name: 'NotFoundError',
  message: 'Resource $id not found'
}) {}

// Result + Option in one natural type
function findUser(id: string): NotFoundError | User | null {
  if (id === 'bad') return new NotFoundError({ id })
  if (id === 'missing') return null
  return { id, name: 'Alice' }
}

const user = findUser('123')

// Handle error first
if (user instanceof Error) {
  return user.message  // TypeScript: user is NotFoundError
}

// Handle null/missing case - use ?. and ?? naturally!
const name = user?.name ?? 'Anonymous'

// Or check explicitly
if (user === null) {
  return 'User not found'
}

// TypeScript knows: user is User
console.log(user.name)

Why this is better than Rust/Zig

| Language | Result + Option | Order matters? | |----------|-----------------|----------------| | Rust | Result<Option<T>, E> or Option<Result<T, E>> | Yes, must unwrap in order | | Zig | !?T (error union + optional) | Yes, specific syntax | | errore | Error \| T \| null | No! Check in any order |

With errore you check in any order:

  • Use ?. and ?? naturally
  • Check instanceof Error or === null in any order
  • No unwrapping ceremony
  • TypeScript infers everything

Why This Is Better Than Go

Go's error handling uses two separate return values:

user, err := fetchUser(id)
// Oops! Forgot to check err
fmt.Println(user.Name)  // Compiles fine, crashes at runtime

The compiler can't save you here. You can ignore err entirely and use user directly.

With errore, forgetting to check is impossible:

const user = await fetchUser(id)  // type: NotFoundError | User

console.log(user.id)  // TS Error: Property 'id' does not exist on type 'NotFoundError'

Since errore uses a single union variable instead of two separate values, TypeScript forces you to narrow the type before accessing value-specific properties. You literally cannot use the value without first doing an instanceof Error check.

Note: Properties that exist on both Error and your value type (like name, message) can still be accessed without narrowing. This is a small set of 4 fields: name, message, stack, cause.

The Remaining Gap

There's still one case errore can't catch: ignored return values:

// Oops! Completely ignoring the return value
updateUser(id, data)  // No error, but we should check!

For this, use TypeScript's built-in checks or a linter:

TypeScript tsconfig.json:

{
  "compilerOptions": {
    "noUnusedLocals": true
  }
}

This catches unused variables, though not ignored return values directly.

oxlint no-unused-expressions:

oxlint.json:

{
  "rules": {
    "no-unused-expressions": "error"
  }
}

Or via CLI:

oxlint --deny no-unused-expressions

Combined with errore's type safety, these tools give you near-complete protection against ignored errors.

Comparison with Result Types

Direct returns vs wrapper methods:

| Result Pattern | errore | |---------------|--------| | Result.ok(value) | just return value | | Result.err(error) | just return error | | result.value | direct access after guard | | result.map(fn) | map(result, fn) | | Result<User, Error> | Error \| User | | Result<Option<T>, E> | Error \| T \| null |

Vs neverthrow / better-result

These libraries wrap values in a Result container. You construct results with ok() and err(), then unwrap them with .value and .error:

// neverthrow
import { ok, err, Result } from 'neverthrow'

function getUser(id: string): Result<User, NotFoundError> {
  const user = db.find(id)
  if (!user) return err(new NotFoundError({ id }))
  return ok(user)  // must wrap
}

const result = getUser('123')
if (result.isErr()) {
  console.log(result.error)  // must unwrap
  return
}
console.log(result.value.name)  // must unwrap
// errore
function getUser(id: string): User | NotFoundError {
  const user = db.find(id)
  if (!user) return new NotFoundError({ id })
  return user  // just return
}

const user = getUser('123')
if (user instanceof Error) {
  console.log(user)  // it's already the error
  return
}
console.log(user.name)  // it's already the user

The key insight: T | Error already encodes success/failure. TypeScript's type narrowing does the rest. No wrapper needed.

| Feature | neverthrow | errore | |---------|------------|--------| | Type-safe errors | ✓ | ✓ | | Exhaustive handling | ✓ | ✓ | | Works with null | Result<T \| null, E> | T \| E \| null | | Learning curve | New API (ok, err, map, andThen, ...) | Just instanceof | | Bundle size | ~3KB min | ~0 bytes | | Interop | Requires wrapping/unwrapping at boundaries | Native TypeScript |

neverthrow also requires an eslint plugin to catch unhandled results. With errore, TypeScript itself prevents you from using a value without checking the error first.

Vs Effect.ts

Effect is not just error handling—it's a complete functional programming framework with dependency injection, concurrency primitives, resource management, streaming, and more.

// Effect.ts - a paradigm shift
import { Effect, pipe } from 'effect'

const program = pipe(
  fetchUser(id),
  Effect.flatMap(user => fetchPosts(user.id)),
  Effect.map(posts => posts.filter(p => p.published)),
  Effect.catchTag('NotFoundError', () => Effect.succeed([]))
)

const result = await Effect.runPromise(program)
// errore - regular TypeScript
const user = await fetchUser(id)
if (user instanceof Error) return []

const posts = await fetchPosts(user.id)
if (posts instanceof Error) return []

return posts.filter(p => p.published)

Effect is powerful if you need its full feature set. But if you just want type-safe errors:

| | Effect | errore | |-|--------|--------| | Learning curve | Steep (new paradigm) | Minimal (just instanceof) | | Codebase impact | Pervasive (everything becomes an Effect) | Surgical (adopt incrementally) | | Bundle size | ~50KB+ | ~0 bytes | | Use case | Full FP framework | Just error handling |

Use Effect when you want dependency injection, structured concurrency, and the full functional programming experience.

Use errore when you just want type-safe errors without rewriting your codebase.

Zero-Dependency Philosophy

errore is more a way of writing code than a library. The core pattern requires nothing:

// You can write this without installing errore at all
class NotFoundError extends Error {
  readonly _tag = 'NotFoundError'
  constructor(public id: string) {
    super(`User ${id} not found`)
  }
}

async function getUser(id: string): Promise<User | NotFoundError> {
  const user = await db.find(id)
  if (!user) return new NotFoundError(id)
  return user
}

const user = await getUser('123')
if (user instanceof Error) return user
console.log(user.name)

The errore package just provides conveniences: createTaggedError for less boilerplate, matchError for exhaustive pattern matching, tryAsync for catching exceptions. But the core pattern—errors as union types—works with zero dependencies.

Perfect for Libraries

Ideal for library authors. Return plain TypeScript unions instead of forcing users to adopt your error handling framework:

// ❌ Library that forces a dependency on users
import { Result } from 'some-result-lib'
export function parse(input: string): Result<AST, ParseError>

// Users must now install and learn 'some-result-lib'
// ✓ Library using plain TypeScript unions
export function parse(input: string): AST | ParseError

// Users handle errors with standard instanceof checks
// No new dependencies, no new concepts to learn

Your library stays lightweight. Users get type-safe errors without adopting an opinionated wrapper. Everyone wins.

Import Style

Note: Always use import * as errore from 'errore' instead of named imports. This makes code easier to move between files, and more readable since every function call is clearly namespaced (e.g. errore.isOk() instead of just isOk()).

License

MIT