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 distinction between what should be handled with Result versus what should be allowed to throw lies in understanding the nature of the error:
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.
ResultAsync <void, type PostDeleteError = PostNotFoundError | PostPermissionError | PostAlreadyDeletedError PostDeleteError > => {
// Business logic errors that should be handled by the application
}These are infrastructure-level or truly unexpected errors:
// 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).
Result.tryHowever, 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.
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, 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',
}) {}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.
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
.
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
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.
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
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.
error (const result: Result.Failure<UnexpectedError> result .error: UnexpectedError error .Error.cause?: unknown cause );
}cause property for debugging purposesThe 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.