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

result-guard

v1.2.3

Published

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

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.