• English
  • 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.

    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.fn

    However, when you want more detailed stack traces for debugging purposes, we recommend using Result.fn 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 = string, Message extends string = string, Fields extends ErrorFields = ErrorFields>(props: {
            name?: Name;
            message: Message | ((fields: Fields) => Message);
            fields?: Fields;
        }): ErrorConstructor<Name, Message, Fields>;
        fields<Fields extends ErrorFields>(): Fields;
    }

    A factory function that creates a base class for custom error types.

    Extend the returned class to define a custom error with a consistent structure, reducing boilerplate and ensuring type safety across your application.

    @typeParamName - Inferred as a string literal type from props.name when provided, or defaults to string when name is omitted.@typeParamMessage - Inferred as a string literal type from props.message when it is a string, or defaults to string when message is a function.@typeParamFields - Inferred from props.fields (via ErrorFactory.fields). Defaults to the base ErrorFields constraint when fields is omitted.@paramprops - Configuration for the error class.@paramprops .name - The value set as the name property on both the class and each instance. When omitted, name is inferred as string and set to new.target.name at construction time, which resolves to the name of the concrete subclass. Note that omitting name disables type narrowing via the name property; use name explicitly or instanceof for narrowing.@paramprops .message - The error message. Can be a static string or a function that receives the custom fields and returns a string, enabling dynamic message generation.@paramprops .fields - A type-level placeholder that declares the additional fields the error instance will carry. Use ErrorFactory.fields to create this value. When omitted, no additional fields are added to the instance.@returnsAn abstract base class typed as ErrorConstructor that should be extended to produce a concrete custom error class.@example

    Basic usage

    class NotFoundError extends ErrorFactory({
    name: 'NotFoundError',
    message: 'Resource not found',
    }) {}
    
    const error = new NotFoundError();
    console.error(error.name);    // "NotFoundError"
    console.error(error.message); // "Resource not found"
    @example

    Omitting name

    class NotFoundError extends ErrorFactory({
    message: 'Resource not found',
    }) {}
    
    const error = new NotFoundError();
    console.error(error.name); // "NotFoundError" (resolved from new.target.name)
    @example

    With cause

    class DatabaseError extends ErrorFactory({
    name: 'DatabaseError',
    message: 'A database error occurred',
    }) {}
    
    const error = new DatabaseError({ cause: new Error('Connection failed') });
    console.error(error.cause); // Error: Connection failed
    @example

    With additional fields

    class QueryError extends ErrorFactory({
    name: 'QueryError',
    message: 'An error occurred while executing a query',
    fields: ErrorFactory.fields<{ query: string }>(),
    }) {}
    
    const error = new QueryError({ query: 'SELECT * FROM users' });
    console.error(error.query); // "SELECT * FROM users"
    @example

    Dynamic message

    class ValidationError extends ErrorFactory({
    name: 'ValidationError',
    message: ({ field }) => `Validation failed for field '${field}'`,
    fields: ErrorFactory.fields<{ field: string }>(),
    }) {}
    
    const error = new ValidationError({ field: 'email' });
    console.error(error.message); // "Validation failed for field 'email'"
    ErrorFactory
    } from '@praha/error-factory';
    class
    class UnexpectedError
    UnexpectedError
    extends
    ErrorFactory<"UnexpectedError", "An unexpected error occurred", ErrorFields>(props: {
        name?: "UnexpectedError" | undefined;
        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";
    }

    A factory function that creates a base class for custom error types.

    Extend the returned class to define a custom error with a consistent structure, reducing boilerplate and ensuring type safety across your application.

    @typeParamName - Inferred as a string literal type from props.name when provided, or defaults to string when name is omitted.@typeParamMessage - Inferred as a string literal type from props.message when it is a string, or defaults to string when message is a function.@typeParamFields - Inferred from props.fields (via ErrorFactory.fields). Defaults to the base ErrorFields constraint when fields is omitted.@paramprops - Configuration for the error class.@paramprops .name - The value set as the name property on both the class and each instance. When omitted, name is inferred as string and set to new.target.name at construction time, which resolves to the name of the concrete subclass. Note that omitting name disables type narrowing via the name property; use name explicitly or instanceof for narrowing.@paramprops .message - The error message. Can be a static string or a function that receives the custom fields and returns a string, enabling dynamic message generation.@paramprops .fields - A type-level placeholder that declares the additional fields the error instance will carry. Use ErrorFactory.fields to create this value. When omitted, no additional fields are added to the instance.@returnsAn abstract base class typed as ErrorConstructor that should be extended to produce a concrete custom error class.@example

    Basic usage

    class NotFoundError extends ErrorFactory({
    name: 'NotFoundError',
    message: 'Resource not found',
    }) {}
    
    const error = new NotFoundError();
    console.error(error.name);    // "NotFoundError"
    console.error(error.message); // "Resource not found"
    @example

    Omitting name

    class NotFoundError extends ErrorFactory({
    message: 'Resource not found',
    }) {}
    
    const error = new NotFoundError();
    console.error(error.name); // "NotFoundError" (resolved from new.target.name)
    @example

    With cause

    class DatabaseError extends ErrorFactory({
    name: 'DatabaseError',
    message: 'A database error occurred',
    }) {}
    
    const error = new DatabaseError({ cause: new Error('Connection failed') });
    console.error(error.cause); // Error: Connection failed
    @example

    With additional fields

    class QueryError extends ErrorFactory({
    name: 'QueryError',
    message: 'An error occurred while executing a query',
    fields: ErrorFactory.fields<{ query: string }>(),
    }) {}
    
    const error = new QueryError({ query: 'SELECT * FROM users' });
    console.error(error.query); // "SELECT * FROM users"
    @example

    Dynamic message

    class ValidationError extends ErrorFactory({
    name: 'ValidationError',
    message: ({ field }) => `Validation failed for field '${field}'`,
    fields: ErrorFactory.fields<{ field: string }>(),
    }) {}
    
    const error = new ValidationError({ field: 'email' });
    console.error(error.message); // "Validation failed for field 'email'"
    ErrorFactory
    ({
    name?: "UnexpectedError" | undefined
    name
    : 'UnexpectedError',
    message: "An unexpected error occurred" | ((fields: ErrorFields) => "An unexpected error occurred")
    message
    : 'An unexpected error occurred',
    }) {}

    Using Result.fn

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

    Wraps a function that may throw and returns a new function that returns a Result or ResultAsync .

    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.@returnsA new function that returns a Result or ResultAsync wrapping the original function's return value or the caught error.@example

    Sync try-catch

    import { Result } from '@praha/byethrow';
    
    const fn = Result.fn({
    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 safe

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

    Async try-catch

    import { Result } from '@praha/byethrow';
    
    const fn = Result.fn({
    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 safe

    import { Result } from '@praha/byethrow';
    
    const fn = Result.fn({
    safe: true,
    try: async () => await Promise.resolve('ok'),
    });
    
    const result = await fn(); // Result.ResultAsync<string, never>
    @categoryCreators
    fn
    ({
    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: <Result.Result<string, UnexpectedError>>(result: Result.Result<string, UnexpectedError>) => result is Result.Failure<UnexpectedError>

    Type guard to check if a Result is a Failure .

    @function@typeParamR - The type of the result to check.@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
    console
    .
    Console.error(...data: any[]): void

    The console.error() static method outputs a message to the console at the "error" log level. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information.

    MDN Reference

    error
    (
    const result: Result.Failure<UnexpectedError>
    result
    .
    error: UnexpectedError
    error
    .
    Error.stack?: string | undefined
    stack
    );
    // Original error is still accessible
    var console: Console
    console
    .
    Console.error(...data: any[]): void

    The console.error() static method outputs a message to the console at the "error" log level. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information.

    MDN Reference

    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. 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.