#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.@exampleimport { 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:
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.@exampleBasic 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"
@exampleOmitting name
class NotFoundError extends ErrorFactory({
message: 'Resource not found',
}) {}
const error = new NotFoundError();
console.error(error.name); // "NotFoundError" (resolved from new.target.name)
@exampleWith 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
@exampleWith 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"
@exampleDynamic 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.@exampleBasic 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"
@exampleOmitting name
class NotFoundError extends ErrorFactory({
message: 'Resource not found',
}) {}
const error = new NotFoundError();
console.error(error.name); // "NotFoundError" (resolved from new.target.name)
@exampleWith 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
@exampleWith 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"
@exampleDynamic 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.@exampleSync 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>
@exampleSync 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>
@exampleAsync 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>
@exampleAsync 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.@exampleimport { 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: ConsoleThe 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: ConsoleThe 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
- Clean Stack Traces: You get stack traces that point to your application code, not deep into library internals
- Error Context: You can add meaningful context to errors while preserving the original error
- Debugging: The original error is still accessible through the
causeproperty 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.
