tryError Documentation

Error Handling Philosophy

Understanding the principles and design decisions behind tryError

Core Principles

Errors are Values

Errors should be treated as first-class values that can be passed around, inspected, and handled explicitly. This makes error handling visible in the type system and forces developers to consider failure cases.

Progressive Enhancement

You shouldn't need to rewrite your entire codebase to get benefits. tryError works alongside existing error handling patterns, allowing gradual adoption.

Minimal Runtime Cost

Error handling shouldn't slow down your application. tryError adds <3% overhead for successful operations. Error paths have configurable overhead (20%-120%) based on debugging needs - this is acceptable because errors should be exceptional.

Developer Experience First

The API should feel natural to JavaScript/TypeScript developers. No need to learn complex functional programming concepts or monadic patterns.

The Problem with Exceptions

Traditional exception handling has several issues that tryError addresses:

❌ Problems with try/catch

  • Invisible in types: Functions don't declare what errors they might throw
  • Easy to ignore: Forgotten catch blocks lead to unhandled exceptions
  • Performance cost: Exception throwing and stack unwinding is expensive
  • Control flow: Exceptions break normal program flow unpredictably
  • Type uncertainty: catch blocks receive unknown or any
Problems with Traditional Error Handlingtypescript
// What errors can this function throw? 🤷‍♂️
async function fetchUserData(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return validateUser(data);
}

// Calling code has no guidance
try {
  const user = await fetchUserData('123');
  // What if the network fails?
  // What if the JSON is invalid?
  // What if validation fails?
} catch (error) {
  // What type is error? What should we do?
  console.error(error);
}

The tryError Solution

✅ Benefits of tryError

  • Visible in types: Return types show both success and error possibilities
  • Explicit handling: You must check for errors before accessing values
  • Minimal overhead: <3% cost for success path, configurable error overhead
  • Predictable flow: Errors are returned, not thrown
  • Type safety: Errors have known structure and properties
tryError Solutiontypescript
1// Clear error handling contract
2async function fetchUserData(id: string): Promise<TryResult<User, TryError>> {
3  const response = await tryAsync(() => fetch(`/api/users/${id}`));
4  if (isTryError(response)) return response;
5  
6  const data = await tryAsync(() => response.json());
7  if (isTryError(data)) return data;
8  
9  const user = trySync(() => validateUser(data));
10  return user; // TryResult<User, TryError>
11}
12
13// Calling code has clear guidance
14const result = await fetchUserData('123');
15if (isTryError(result)) {
16  // Handle specific error types
17  console.error('Failed to fetch user:', result.message);
18  return;
19}
20
21// TypeScript knows result is User here
22console.log('User name:', result.name);

Design Decisions

Why Union Types Instead of Monads?

While functional programming languages often use Result/Either monads, tryError uses simple union types because they're more familiar to JavaScript developers and integrate better with existing TypeScript patterns.

Union Types vs Monadstypescript
// Simple union type - familiar to TS developers
type TryResult<T, E> = T | E;

// vs. Monadic approach - requires learning new patterns
interface Result<T, E> {
  map<U>(fn: (value: T) => U): Result<U, E>;
  flatMap<U>(fn: (value: T) => Result<U, E>): Result<U, E>;
  // ... many more methods
}

Why Not Just Use Result Libraries?

Existing Result libraries often require a paradigm shift and have steep learning curves. tryError provides similar benefits with a more approachable API that feels natural to JavaScript developers.

Rich Error Context (With Trade-offs)

tryError errors include rich context like stack traces, timestamps, and source information to aid in debugging. This causes higher error path overhead (20%-120%) but is configurable. The trade-off is worth it because errors should be rare, and debugging time saved outweighs runtime cost.

TryError Interfacetypescript
interface TryError {
  readonly type: 'TryError';
  readonly message: string;
  readonly stack?: string;
  readonly source: string;
  readonly timestamp: number;
  readonly context?: Record<string, unknown>;
  readonly cause?: Error | TryError;
}

When to Use tryError

✅ Great for:

  • • API calls and network operations
  • • File system operations
  • • Data parsing and validation
  • • Database operations
  • • User input processing
  • • Configuration loading
  • • Any operation that might fail

⚠️ Consider alternatives for:

  • • Programming errors (use assertions)
  • • Truly exceptional conditions
  • • Library code that needs to integrate with existing exception-based APIs
  • • Performance-critical hot paths where even type checking matters

Next Steps

TryResult vs Exceptions

Detailed comparison with traditional error handling

Compare Approaches →

Error Types

Understanding TryError structure and custom errors

Learn Error Types →

Quick Start

Start using tryError in your project

Get Started →