JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 2
  • Score
    100M100P100Q31924F
  • License MIT

Type-safe error handling with discriminated unions and type guards for TypeScript

Package Exports

  • result-guard

Readme

result-guard

A TypeScript utility for elegant, type-safe error handling. It wraps your code in a Result type that makes error handling explicit and type-safe, eliminating the need for try-catch blocks while maintaining full type information.

Table of Contents

Features

  • 🎯 Type-Safe: Full TypeScript support with discriminated unions and type guards
  • 🔄 Universal: Works with both sync and async code
  • 🛡️ Robust: Automatically converts thrown values to proper Error objects
  • 🧬 Preserves: Keeps error stack traces and inheritance chains intact
  • 🎨 Flexible: Supports custom error types
  • Performant: Zero dependencies, lightweight implementation
  • 🔍 Developer Friendly: Great TypeScript inference and detailed error info
  • 📦 Module Support: Works with both ESM and CommonJS

Installation

npm install result-guard

Module Support

result-guard supports both ESM (ECMAScript Modules) and CommonJS:

// ESM
import { tryCatch, isSuccess } from 'result-guard';

// CommonJS
const { tryCatch, isSuccess } = require('result-guard');

The package automatically uses the correct format based on your project's configuration:

  • If your package.json has "type": "module", it uses ESM
  • If not specified, it uses CommonJS
  • You can also explicitly import the ESM version using the .mjs extension or import field

Quick Start

import { tryCatch, isSuccess } from 'result-guard';

// Sync example
const result = tryCatch(() => "hello world");
if (isSuccess(result)) {
  console.log(result.data); // TypeScript knows this is string
}

// Async example
const fetchUser = async (id: string) => {
  const result = await tryCatch(async () => {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  });

  if (isSuccess(result)) {
    return result.data; // Success case
  }
  // Error case - result.error is typed as Error
  console.error('Failed to fetch user:', result.error.message);
  return null;
};

Core Concepts

The Result Type

The Result type is a discriminated union that represents either success or failure:

type Result<T, E = Error> = 
  | { data: T; error: null; isError: false }  // Success case
  | { data: null; error: E; isError: true }   // Failure case

// Example usage:
const divide = (a: number, b: number): Result<number> => {
  if (b === 0) {
    return { data: null, error: new Error("Division by zero"), isError: true };
  }
  return { data: a / b, error: null, isError: false };
};

const result = divide(10, 2);
if (!result.isError) {
  console.log(result.data); // TypeScript knows this is number
}

Type Guards

Type guards help TypeScript narrow down the type:

import { isSuccess, isFailure } from 'result-guard';

const result = tryCatch(() => "hello");

// TypeScript knows result.data is string here
if (isSuccess(result)) {
  console.log(result.data.toUpperCase());
}

// TypeScript knows result.error is Error here
if (isFailure(result)) {
  console.log(result.error.message);
}

Custom Error Types

You can use your own error types for better error handling:

class ApiError extends Error {
  constructor(
    public statusCode: number,
    message: string
  ) {
    super(message);
  }
}

// Specify the error type as ApiError
const result = await tryCatch<Response, ApiError>(async () => {
  const response = await fetch('/api/data');
  if (!response.ok) {
    throw new ApiError(response.status, response.statusText);
  }
  return response;
});

if (isFailure(result)) {
  // TypeScript knows result.error is ApiError
  console.log(`API Error ${result.error.statusCode}: ${result.error.message}`);
}

Common Patterns

Early Return Pattern

Best for functions that should stop on error:

async function processUserData(userId: string) {
  // Get user
  const userResult = await tryCatch(async () => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  });

  if (isFailure(userResult)) {
    return { error: `Failed to fetch user: ${userResult.error.message}` };
  }

  // Get user's posts
  const postsResult = await tryCatch(async () => {
    const response = await fetch(`/api/users/${userId}/posts`);
    return response.json();
  });

  if (isFailure(postsResult)) {
    return { error: `Failed to fetch posts: ${postsResult.error.message}` };
  }

  // Success case - both operations succeeded
  return {
    user: userResult.data,
    posts: postsResult.data
  };
}

Destructuring Pattern

Good for simple cases where you want to handle both success and error inline:

async function getLatestPost() {
  const { data: post, error } = await tryCatch(async () => {
    const response = await fetch('/api/posts/latest');
    return response.json();
  });

  if (error) {
    console.error('Failed to fetch post:', error);
    return null;
  }

  return post;
}

Parallel Operations Pattern

Handle multiple operations that can succeed or fail independently:

async function getDashboardData() {
  const [usersResult, postsResult, statsResult] = await Promise.all([
    tryCatch(() => fetch('/api/users').then(r => r.json())),
    tryCatch(() => fetch('/api/posts').then(r => r.json())),
    tryCatch(() => fetch('/api/stats').then(r => r.json()))
  ]);

  return {
    users: isSuccess(usersResult) ? usersResult.data : [],
    posts: isSuccess(postsResult) ? postsResult.data : [],
    stats: isSuccess(statsResult) ? statsResult.data : null,
    errors: [
      isFailure(usersResult) && 'Failed to load users',
      isFailure(postsResult) && 'Failed to load posts',
      isFailure(statsResult) && 'Failed to load stats'
    ].filter(Boolean)
  };
}

Utility Functions

Working with Events (withEvents)

Safely handle event emitters and streams:

import { withEvents } from 'result-guard';
import { createReadStream } from 'fs';

async function readFileContents(filePath: string) {
  const stream = createReadStream(filePath);
  
  const result = await withEvents(
    stream,
    async () => {
      const chunks: Buffer[] = [];
      for await (const chunk of stream) {
        chunks.push(chunk);
      }
      return Buffer.concat(chunks).toString('utf8');
    },
    {
      timeout: 5000, // 5 second timeout
      cleanup: () => stream.destroy(), // Clean up the stream
      errorEvent: 'error' // Listen for 'error' events
    }
  );

  if (isSuccess(result)) {
    return result.data;
  }
  throw new Error(`Failed to read file: ${result.error.message}`);
}

Processing Iterators (withIterator)

Safely process async iterators with timeout and early termination:

import { withIterator } from 'result-guard';

async function processLargeDataSet() {
  async function* dataGenerator() {
    let page = 1;
    while (true) {
      const response = await fetch(`/api/data?page=${page}`);
      const data = await response.json();
      if (data.length === 0) break;
      yield* data;
      page++;
    }
  }

  const result = await withIterator(dataGenerator(), {
    timeout: 30000, // 30 second timeout
    maxItems: 1000, // Stop after 1000 items
    onItem: (item) => {
      // Stop if we find an invalid item
      if (!item.isValid) return false;
      // Continue processing
      return true;
    }
  });

  if (isSuccess(result)) {
    return result.data;
  }
  console.error('Failed to process data:', result.error);
  return [];
}

Handling Callbacks (withCallbacks)

Convert callback-style APIs to promises:

import { withCallbacks } from 'result-guard';
import { Database } from 'some-db-library';

function queryDatabase(sql: string, params: any[]) {
  return withCallbacks<any[]>(({ resolve, reject }) => {
    const db = new Database();
    
    db.query(sql, params, (err, results) => {
      if (err) reject(err);
      else resolve(results);
    });

    // Return cleanup function
    return () => db.close();
  }, {
    timeout: 5000 // 5 second timeout
  });
}

// Usage
const result = await queryDatabase('SELECT * FROM users WHERE id = ?', [123]);
if (isSuccess(result)) {
  console.log('Query results:', result.data);
}

Running Concurrent Operations (concurrent)

Execute multiple operations with controlled concurrency and precise type inference:

// Example with typed functions
interface User { name: string; id: number }
interface Post { title: string; content: string }

const getUser = async (): Promise<User> => ({ name: 'bob', id: 1 });
const getPost = async (): Promise<Post> => ({ 
  title: 'Hello',
  content: 'World'
});

// TypeScript infers exact return types
const results = await concurrent([
  getUser,
  getPost
] as const);

const [userResult, postResult] = results;

if (!userResult.isError) {
  const user = userResult.data; // TypeScript knows this is User
  console.log(user.name, user.id);
}

if (!postResult.isError) {
  const post = postResult.data; // TypeScript knows this is Post
  console.log(post.title, post.content);
}

// Example with literal types
const literalResults = await concurrent([
  async () => 42 as const,
  async () => 'hello' as const,
  async () => ({ status: 'ok' as const })
] as const);

const [numResult, strResult, objResult] = literalResults;

if (!numResult.isError) {
  const num = numResult.data; // Type is exactly 42
  console.log(num); // TypeScript knows this is exactly 42
}

if (!strResult.isError) {
  const str = strResult.data; // Type is exactly 'hello'
  console.log(str); // TypeScript knows this is exactly 'hello'
}

if (!objResult.isError) {
  const obj = objResult.data; // Type is exactly { status: 'ok' }
  console.log(obj.status); // TypeScript knows this is exactly 'ok'
}

// With concurrency control
const results = await concurrent(
  [getUser, getPost],
  {
    timeout: 5000, // 5 second timeout
    maxConcurrent: 2, // Run at most 2 operations at once
    stopOnError: false // Continue on error
  }
);

The concurrent function provides:

  • Precise type inference for each operation's return type
  • Support for both typed functions and literal types
  • Controlled concurrency with maxConcurrent
  • Timeout handling for long-running operations
  • Error handling with stopOnError option
  • Type-safe access to results through destructuring

Composing Utilities

The utility functions can be composed together using the concurrent function. Here's how to combine multiple utilities:

import { concurrent, withEvents, withIterator, withCallbacks } from 'result-guard';
import { EventEmitter } from 'events';

// Example combining multiple utilities
async function processMultipleOperations() {
  // Create event emitters for testing
  const emitter1 = new EventEmitter();
  const emitter2 = new EventEmitter();

  // Define an async iterator
  async function* numberGenerator() {
    for (let i = 1; i <= 3; i++) {
      yield i;
      await new Promise(resolve => setTimeout(resolve, 1));
    }
  }

  // Run multiple operations concurrently
  const results = await concurrent([
    // Process first event emitter
    async () => {
      const result = await withEvents(
        emitter1,
        () => new Promise<string>(resolve => {
          emitter1.once('data', resolve);
          setTimeout(() => emitter1.emit('data', 'event1 data'), 1);
        })
      );
      return result.isError ? Promise.reject(result.error) : result.data;
    },

    // Process second event emitter
    async () => {
      const result = await withEvents(
        emitter2,
        () => new Promise<string>(resolve => {
          emitter2.once('data', resolve);
          setTimeout(() => emitter2.emit('data', 'event2 data'), 1);
        })
      );
      return result.isError ? Promise.reject(result.error) : result.data;
    },

    // Process an async iterator
    async () => {
      const result = await withIterator(numberGenerator());
      return result.isError ? Promise.reject(result.error) : result.data;
    },

    // Handle callbacks
    async () => {
      const result = await withCallbacks<string>(({ resolve }) => {
        const timeoutId = setTimeout(() => resolve('callback data'), 1);
        return () => clearTimeout(timeoutId);
      });
      return result.isError ? Promise.reject(result.error) : result.data;
    }
  ] as const);

  // Destructure and handle results
  const [event1Result, event2Result, iteratorResult, callbackResult] = results;

  return {
    event1: !event1Result.isError ? event1Result.data : null,
    event2: !event2Result.isError ? event2Result.data : null,
    numbers: !iteratorResult.isError ? iteratorResult.data : [],
    callbackData: !callbackResult.isError ? callbackResult.data : null,
    errors: results
      .filter(r => r.isError)
      .map(r => r.error.message)
  };
}

// Error handling example
async function handleErrors() {
  const emitter = new EventEmitter();
  
  const results = await concurrent([
    // Event emitter that errors
    async () => {
      const result = await withEvents(
        emitter,
        () => new Promise<string>((_, reject) => {
          emitter.once('error', reject);
          setTimeout(() => emitter.emit('error', new Error('event error')), 1);
        })
      );
      return result.isError ? Promise.reject(result.error) : result.data;
    },

    // Iterator that errors
    async () => {
      const result = await withIterator(async function* () {
        yield 1;
        throw new Error('iterator error');
      }());
      return result.isError ? Promise.reject(result.error) : result.data;
    },

    // Callback that errors
    async () => {
      const result = await withCallbacks<string>(({ reject }) => {
        const timeoutId = setTimeout(() => reject(new Error('callback error')), 1);
        return () => clearTimeout(timeoutId);
      });
      return result.isError ? Promise.reject(result.error) : result.data;
    }
  ] as const);

  // All operations should have failed
  const errors = results
    .filter(r => r.isError)
    .map(r => r.error.message);

  return errors; // ['event error', 'iterator error', 'callback error']
}

// Cleanup example
async function handleCleanup() {
  const cleanupCalls = {
    event: 0,
    iterator: 0,
    callback: 0
  };

  const emitter = new EventEmitter();
  
  await concurrent([
    // Event with cleanup
    async () => {
      const result = await withEvents(
        emitter,
        () => Promise.resolve('event data'),
        {
          cleanup: () => {
            cleanupCalls.event++;
          }
        }
      );
      return result.isError ? Promise.reject(result.error) : result.data;
    },

    // Iterator with cleanup
    async () => {
      const result = await withIterator((async function* () {
        try {
          yield 1;
        } finally {
          cleanupCalls.iterator++;
        }
      })());
      return result.isError ? Promise.reject(result.error) : result.data;
    },

    // Callback with cleanup
    async () => {
      const result = await withCallbacks<string>(({ resolve }) => {
        const timeoutId = setTimeout(() => resolve('callback data'), 1);
        return () => {
          clearTimeout(timeoutId);
          cleanupCalls.callback++;
        };
      });
      return result.isError ? Promise.reject(result.error) : result.data;
    }
  ] as const);

  return cleanupCalls; // { event: 1, iterator: 1, callback: 1 }
}

This composition pattern provides:

  • Type-safe composition of multiple utility functions
  • Proper error propagation across all utilities
  • Independent cleanup handling for each operation
  • Concurrent execution with controlled concurrency
  • Consistent error handling patterns
  • Resource cleanup in all cases (success, error, timeout)

The key benefits of this approach are:

  1. Type Safety: Full TypeScript support with proper type inference
  2. Error Handling: Unified error handling across different types of operations
  3. Resource Management: Guaranteed cleanup of resources
  4. Concurrency Control: Ability to run operations in parallel with limits
  5. Flexibility: Mix and match different utilities as needed

The concurrent function provides:

  • Precise type inference for each operation's return type
  • Support for both typed functions and literal types
  • Controlled concurrency with maxConcurrent
  • Timeout handling for long-running operations
  • Error handling with stopOnError option
  • Type-safe access to results through destructuring

Piping Operations (pipe)

Compose operations in a sequential chain, passing results from one operation to the next:

import { pipe, tryCatch } from 'result-guard';

async function fetchUserData(userId: string) {
  // Chain operations in a pipeline
  const result = await pipe(
    userId,
    [
      // First, get the user
      (id) => tryCatch(() => {
        return fetch(`/api/users/${id}`)
          .then(res => {
            if (!res.ok) throw new Error(`Failed to fetch user: ${res.status}`);
            return res.json();
          });
      }),
      
      // Then, get their posts using the user data
      (user) => tryCatch(() => {
        return fetch(`/api/users/${user.id}/posts`)
          .then(res => {
            if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`);
            return res.json();
          });
      }),
      
      // Finally, process the posts
      (posts) => tryCatch(() => {
        return posts.map(post => ({
          title: post.title,
          excerpt: post.body.substring(0, 100) + '...'
        }));
      })
    ]
  );

  if (result.isError) {
    console.error("Error in pipeline:", result.error.message);
    return null;
  }
  
  return result.data;
}

The pipe function:

  • Takes an initial value and an array of operations
  • Passes each successful result as input to the next operation
  • Short-circuits on the first error
  • Supports both synchronous and asynchronous operations
  • Preserves full type safety throughout the chain
  • Provides a clean, functional programming approach to sequential operations

This pattern is ideal for:

  • Sequential API calls that depend on previous results
  • Data transformations that need to happen in a specific order
  • Complex validation chains
  • Building up results through a series of transformations

Configuration Types

Common Options

All utility functions accept a timeout option:

type TimeoutOptions = {
  timeout?: number; // Milliseconds before operation times out
};

Event Handler Options

Options for withEvents:

type EventOptions = TimeoutOptions & {
  errorEvent?: string; // Event name to listen for errors (default: 'error')
  cleanup?: () => void | Promise<void>; // Cleanup function
};

Iterator Options

Options for withIterator:

type IteratorOptions<T> = TimeoutOptions & {
  maxItems?: number; // Maximum number of items to process
  onItem?: (item: T) => boolean | Promise<boolean>; // Return false to stop
};

Concurrent Operation Options

Options for concurrent:

type ConcurrentOptions = TimeoutOptions & {
  maxConcurrent?: number; // Maximum parallel operations
  stopOnError?: boolean; // Stop all operations on first error
};

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.