logo
byethrow
  • Guide
  • Examples
  • API Reference
    Getting Started
    Introduction
    Why byethrow?
    Quick Start
    Importing Result
    Best Practices
    Result vs throw
    Custom Error
    Pattern Matching
    Tools & Integrations
    MCP Server
    ESLint Plugin
    📝 Edit this page

    Last Updated: 11/28/2025, 5:42:58 AM

    Previous pageImporting ResultNext pageCustom Error

    #Result vs throw

    You may have encountered opinions like: "Since JavaScript can throw errors anywhere and it's impossible to manage everything with Result, there's no point in introducing Result at all."

    However, we don't necessarily agree with this perspective. The key insight is that Result should only handle "anticipated errors" - there's no need to wrap every possible error in a Result.

    #The Philosophy: Anticipated vs Unexpected Errors

    The distinction between what should be handled with Result versus what should be allowed to throw lies in understanding the nature of the error:

    #Anticipated Errors (Use Result)

    These are errors that are part of your application's business logic and should be handled explicitly:

    // Example of a post deletion function
    type 
    type PostDeleteError = PostNotFoundError | PostPermissionError | PostAlreadyDeletedError
    PostDeleteError
    = (
    |
    class PostNotFoundError
    PostNotFoundError
    |
    class PostPermissionError
    PostPermissionError
    |
    class PostAlreadyDeletedError
    PostAlreadyDeletedError
    ); const
    const deletePost: (postId: string) => Result.ResultAsync<void, PostDeleteError>
    deletePost
    = async (
    postId: string
    postId
    : string):
    import Result
    Result
    .
    type ResultAsync<T, E> = Promise<Result.Result<T, E>>

    An asynchronous variant of Result , wrapped in a Promise.

    @typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@example
    import { Result } from '@praha/byethrow';
    
    const fetchData = async (): Result.ResultAsync<string, Error> => {
      try {
        const data = await fetch('...');
        return { type: 'Success', value: await data.text() };
      } catch (err) {
        return { type: 'Failure', error: err as Error };
      }
    };
    @categoryCore Types
    ResultAsync
    <void,
    type PostDeleteError = PostNotFoundError | PostPermissionError | PostAlreadyDeletedError
    PostDeleteError
    > => {
    // Business logic errors that should be handled by the application }

    #Unexpected Errors (Let them throw)

    These are infrastructure-level or truly unexpected errors:

    • Database connection failures
    • Network timeouts
    • Out of memory errors
    • Unknown exceptions
    // Example of an infrastructure-level function
    const 
    const connectToDatabase: () => Promise<Database>
    connectToDatabase
    = async ():
    interface Promise<T>

    Represents the completion of an asynchronous operation

    Promise
    <Database> => {
    // This function may throw errors like connection failures, timeouts, etc. };

    These should be allowed to throw and be caught by infrastructure-level error handling (like Sentry).

    #When You Need Better Stack Traces: Using Result.try

    However, when you want more detailed stack traces for debugging purposes, we recommend using Result.try to wrap unexpected errors with custom error classes. This approach gives you application-level stack traces instead of library-level ones.

    #Defining Custom Error Classes

    First, define a custom error class for unexpected errors:

    Tip

    For more details about @praha/error-factory, see the Custom Error page.

    import { 
    const ErrorFactory: {
        <Name extends string, Message extends string, Fields extends ErrorFields>(props: {
            name: Name;
            message: Message | ((fields: Fields) => Message);
            fields?: Fields;
        }): ErrorConstructor<Name, Message, Fields>;
        fields<Fields extends ErrorFields>(): Fields;
    }
    ErrorFactory
    } from '@praha/error-factory';
    class
    class UnexpectedError
    UnexpectedError
    extends
    ErrorFactory<"UnexpectedError", "An unexpected error occurred", ErrorFields>(props: {
        name: "UnexpectedError";
        message: "An unexpected error occurred" | ((fields: ErrorFields) => "An unexpected error occurred");
        fields?: ErrorFields | undefined;
    }): (new (options?: ErrorOptions) => Error & Readonly<{
        name: "UnexpectedError";
        message: "An unexpected error occurred";
    }>) & {
        name: "UnexpectedError";
    }
    ErrorFactory
    ({
    name: "UnexpectedError"
    name
    : 'UnexpectedError',
    message: "An unexpected error occurred" | ((fields: ErrorFields) => "An unexpected error occurred")
    message
    : 'An unexpected error occurred',
    }) {}

    #Using Result.try for Better Error Handling

    import { 
    import Result
    Result
    } from '@praha/byethrow';
    // Wrap potentially throwing operations const
    const safeDatabaseOperation: (id: string) => Result.ResultAsync<string, UnexpectedError>
    safeDatabaseOperation
    =
    import Result
    Result
    .
    try<(id: string) => Promise<string>, UnexpectedError>(options: {
        try: (id: string) => Promise<string>;
        catch: (error: unknown) => UnexpectedError;
    }): (id: string) => Result.ResultAsync<string, UnexpectedError> (+7 overloads)
    export try

    Wraps a function execution (sync or async) or a Promise in a Result or ResultAsync type, capturing errors and returning them in a structured way.

    You can use either a custom catch handler or rely on the safe: true option to assume the function cannot throw.

    @function@typeParamT - The function type to execute (sync or async) or a Promise type.@typeParamE - The error type to return if catch is used.@example

    Sync try-catch

    import { Result } from '@praha/byethrow';
    
    const fn = Result.try({
    try: (x: number) => {
    if (x < 0) throw new Error('Negative!');
    return x * 2;
    },
    catch: (error) => new Error('Oops!', { cause: error }),
    });
    
    const result = fn(5); // Result.Result<number, Error>
    @example

    Sync try-catch with immediate execution

    import { Result } from '@praha/byethrow';
    
    const result = Result.try({
    immediate: true,
    try: () => {
    const x = Math.random() * 10 - 5;
    if (x < 0) throw new Error('Negative!');
    return x * 2;
    },
    catch: (error) => new Error('Oops!', { cause: error }),
    });
    
    // result is Result<number, Error>
    @example

    Sync safe

    import { Result } from '@praha/byethrow';
    
    const fn = Result.try({
    safe: true,
    try: (x: number) => x + 1,
    });
    
    const result = fn(1); // Result.Result<number, never>
    @example

    Sync safe with immediate execution

    import { Result } from '@praha/byethrow';
    
    const result = Result.try({
    safe: true,
    immediate: true,
    try: () => Math.random() + 1,
    });
    
    // result is Result<number, never>
    @example

    Async try-catch

    import { Result } from '@praha/byethrow';
    
    const fn = Result.try({
    try: async (id: string) => await fetch(`/api/data/${id}`),
    catch: (error) => new Error('Oops!', { cause: error }),
    });
    
    const result = await fn('abc'); // Result.ResultAsync<Response, Error>
    @example

    Async try-catch with immediate execution

    import { Result } from '@praha/byethrow';
    
    const result = Result.try({
    immediate: true,
    try: () => fetch('/api/data'),
    catch: (error) => new Error('Fetch failed', { cause: error }),
    });
    
    // result is ResultAsync<Response, Error>
    @example

    Async safe

    import { Result } from '@praha/byethrow';
    
    const fn = Result.try({
    safe: true,
    try: async () => await Promise.resolve('ok'),
    });
    
    const result = await fn(); // Result.ResultAsync<string, never>
    @example

    Async safe with immediate execution

    import { Result } from '@praha/byethrow';
    
    const result = Result.try({
    safe: true,
    immediate: true,
    try: () => Promise.resolve('ok'),
    });
    
    // result is ResultAsync<string, never>
    @categoryCreators
    try
    ({
    try: (id: string) => Promise<string>
    try
    : (
    id: string
    id
    : string) => {
    // This might throw database query errors, network errors, etc. return
    const performDatabaseOperation: (id: string) => Promise<string>
    performDatabaseOperation
    (
    id: string
    id
    );
    },
    catch: (error: unknown) => UnexpectedError
    catch
    : (
    error: unknown
    error
    ) => new
    constructor UnexpectedError(options?: ErrorOptions): UnexpectedError
    UnexpectedError
    ({
    ErrorOptions.cause?: unknown
    cause
    :
    error: unknown
    error
    }),
    }); // Usage const
    const result: Result.Result<string, UnexpectedError>
    result
    = await
    const safeDatabaseOperation: (id: string) => Result.ResultAsync<string, UnexpectedError>
    safeDatabaseOperation
    ('123');
    if (
    import Result
    Result
    .
    const isFailure: <UnexpectedError>(result: Result.Result<unknown, UnexpectedError>) => result is Result.Failure<UnexpectedError>

    Type guard to check if a Result is a Failure .

    @function@typeParamE - The type of the error value.@paramresult - The Result to check.@returnstrue if the result is a Failure, otherwise false.@example
    import { Result } from '@praha/byethrow';
    
    const result: Result.Result<number, string> = { type: 'Failure', error: 'Something went wrong' };
    if (Result.isFailure(result)) {
      console.error(result.error); // Safe access to error
    }
    @categoryType Guards
    isFailure
    (
    const result: Result.Result<string, UnexpectedError>
    result
    )) {
    // You now have a clean UnexpectedError with your application's stack trace // instead of deep library stack traces
    var console: Console

    The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

    The module exports two specific components:

    • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
    • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

    Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

    Example using the global console:

    console.log('hello world');
    // Prints: hello world, to stdout
    console.log('hello %s', 'world');
    // Prints: hello world, to stdout
    console.error(new Error('Whoops, something bad happened'));
    // Prints error message and stack trace to stderr:
    //   Error: Whoops, something bad happened
    //     at [eval]:5:15
    //     at Script.runInThisContext (node:vm:132:18)
    //     at Object.runInThisContext (node:vm:309:38)
    //     at node:internal/process/execution:77:19
    //     at [eval]-wrapper:6:22
    //     at evalScript (node:internal/process/execution:76:60)
    //     at node:internal/main/eval_string:23:3
    
    const name = 'Will Robinson';
    console.warn(`Danger ${name}! Danger!`);
    // Prints: Danger Will Robinson! Danger!, to stderr

    Example using the Console class:

    const out = getStreamSomehow();
    const err = getStreamSomehow();
    const myConsole = new console.Console(out, err);
    
    myConsole.log('hello world');
    // Prints: hello world, to out
    myConsole.log('hello %s', 'world');
    // Prints: hello world, to out
    myConsole.error(new Error('Whoops, something bad happened'));
    // Prints: [Error: Whoops, something bad happened], to err
    
    const name = 'Will Robinson';
    myConsole.warn(`Danger ${name}! Danger!`);
    // Prints: Danger Will Robinson! Danger!, to err
    @seesource
    console
    .
    Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)

    Prints to stderr with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

    const code = 5;
    console.error('error #%d', code);
    // Prints: error #5, to stderr
    console.error('error', code);
    // Prints: error 5, to stderr

    If formatting elements (e.g. %d) are not found in the first string then util.inspect() is called on each argument and the resulting string values are concatenated. See util.format() for more information.

    @sincev0 .1.100
    error
    (
    const result: Result.Failure<UnexpectedError>
    result
    .
    error: UnexpectedError
    error
    .
    Error.stack?: string | undefined
    stack
    );
    // Original error is still accessible
    var console: Console

    The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

    The module exports two specific components:

    • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
    • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

    Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

    Example using the global console:

    console.log('hello world');
    // Prints: hello world, to stdout
    console.log('hello %s', 'world');
    // Prints: hello world, to stdout
    console.error(new Error('Whoops, something bad happened'));
    // Prints error message and stack trace to stderr:
    //   Error: Whoops, something bad happened
    //     at [eval]:5:15
    //     at Script.runInThisContext (node:vm:132:18)
    //     at Object.runInThisContext (node:vm:309:38)
    //     at node:internal/process/execution:77:19
    //     at [eval]-wrapper:6:22
    //     at evalScript (node:internal/process/execution:76:60)
    //     at node:internal/main/eval_string:23:3
    
    const name = 'Will Robinson';
    console.warn(`Danger ${name}! Danger!`);
    // Prints: Danger Will Robinson! Danger!, to stderr

    Example using the Console class:

    const out = getStreamSomehow();
    const err = getStreamSomehow();
    const myConsole = new console.Console(out, err);
    
    myConsole.log('hello world');
    // Prints: hello world, to out
    myConsole.log('hello %s', 'world');
    // Prints: hello world, to out
    myConsole.error(new Error('Whoops, something bad happened'));
    // Prints: [Error: Whoops, something bad happened], to err
    
    const name = 'Will Robinson';
    myConsole.warn(`Danger ${name}! Danger!`);
    // Prints: Danger Will Robinson! Danger!, to err
    @seesource
    console
    .
    Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)

    Prints to stderr with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

    const code = 5;
    console.error('error #%d', code);
    // Prints: error #5, to stderr
    console.error('error', code);
    // Prints: error 5, to stderr

    If formatting elements (e.g. %d) are not found in the first string then util.inspect() is called on each argument and the resulting string values are concatenated. See util.format() for more information.

    @sincev0 .1.100
    error
    (
    const result: Result.Failure<UnexpectedError>
    result
    .
    error: UnexpectedError
    error
    .
    Error.cause?: unknown
    cause
    );
    }

    #Benefits of This Approach

    1. Clean Stack Traces: You get stack traces that point to your application code, not deep into library internals
    2. Error Context: You can add meaningful context to errors while preserving the original error
    3. Consistent Error Handling: All errors, whether anticipated or unexpected, can be handled through the Result interface when needed
    4. Debugging: The original error is still accessible through the cause property for debugging purposes

    #Conclusion

    The goal isn't to eliminate all throws in favor of Result, but to use each approach where it's most appropriate. Result excels at handling expected, business-level errors that require explicit handling, while throw remains the right choice for unexpected system errors that should be handled at the infrastructure level.

    This hybrid approach gives you the benefits of explicit error handling where it matters most, without the burden of wrapping every possible error in your application.