#Result Type
Before diving into the API, let's understand what the Result type is and why it's useful.
#What is a Result?
A Result is a type that represents the outcome of an operation that might fail.
It can be one of two variants:
- Success: The operation succeeded and contains a value
- Failure: The operation failed and contains an error
This is fundamentally different from throwing exceptions. With Result, the possibility of failure is encoded in the type system itself.
#The Structure
A Result is a simple object with a type discriminator:
import { import Result Result } from '@praha/byethrow';
// A Success result
const const success: Result.Success<number> success : import Result Result .type Success<T> = {
readonly type: "Success";
readonly value: T;
}
Represents a successful result.
@typeParamT - The type of the successful value.@exampleimport { Result } from '@praha/byethrow';
const success: Result.Success<number> = {
type: 'Success',
value: 42,
};
@categoryCore Types Success <number> = {
type: "Success" type : 'Success',
value: number value : 42,
};
// A Failure result
const const failure: Result.Failure<string> failure : import Result Result .type Failure<E> = {
readonly type: "Failure";
readonly error: E;
}
Represents a failed result.
@typeParamE - The type of the error.@exampleimport { Result } from '@praha/byethrow';
const failure: Result.Failure<string> = {
type: 'Failure',
error: 'Something went wrong',
};
@categoryCore Types Failure <string> = {
type: "Failure" type : 'Failure',
error: string error : 'Something went wrong',
};#The Union Type
The Result.Result<T, E> type is a union of Success<T> and Failure<E>:
import { import Result Result } from '@praha/byethrow';
// This function returns either a Success<number> or a Failure<string>
const const divide: (a: number, b: number) => Result.Result<number, string> divide = (a: number a : number, b: number b : number): import Result Result .type Result<T, E> = Result.Success<T> | Result.Failure<E>A union type representing either a success or a failure.
@typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@exampleimport { Result } from '@praha/byethrow';
const doSomething = (): Result.Result<number, string> => {
return Math.random() > 0.5
? { type: 'Success', value: 10 }
: { type: 'Failure', error: 'Oops' };
};
@categoryCore Types Result <number, string> => {
if (b: number b === 0) {
return { type: "Failure" type : 'Failure', error: string error : 'Cannot divide by zero' };
}
return { type: "Success" type : 'Success', value: number value : a: number a / b: number b };
};
const const result: Result.Result<number, string> result = const divide: (a: number, b: number) => Result.Result<number, string> divide (10, 2);
// Type: Result.Result<number, string>#Why Use Result Instead of Exceptions?
#1. Explicit Error Handling
With exceptions, you never know if a function might throw:
// ❌ Does this throw? We can't tell from the signature
const const parseConfig: (path: string) => Config parseConfig = (path: string path : string): type Config = {
host: string;
port: number;
}
Config => {
// ...
}With Result, it's clear:
// ✅ The return type tells us this might fail
const const parseConfig: (path: string) => Result.Result<Config, ParseError> parseConfig = (path: string path : string): import Result Result .type Result<T, E> = Result.Success<T> | Result.Failure<E>A union type representing either a success or a failure.
@typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@exampleimport { Result } from '@praha/byethrow';
const doSomething = (): Result.Result<number, string> => {
return Math.random() > 0.5
? { type: 'Success', value: 10 }
: { type: 'Failure', error: 'Oops' };
};
@categoryCore Types Result <type Config = {
host: string;
port: number;
}
Config , class ParseError ParseError > => {
// ...
}#2. Type-Safe Errors
Exceptions lose type information. Result preserves it:
import { import Result Result } from '@praha/byethrow';
type type ValidationError = {
field: string;
message: string;
}
ValidationError = { field: string field : string; message: string message : string };
const const validateEmail: (email: string) => Result.Result<string, ValidationError> validateEmail = (email: string email : string): import Result Result .type Result<T, E> = Result.Success<T> | Result.Failure<E>A union type representing either a success or a failure.
@typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@exampleimport { Result } from '@praha/byethrow';
const doSomething = (): Result.Result<number, string> => {
return Math.random() > 0.5
? { type: 'Success', value: 10 }
: { type: 'Failure', error: 'Oops' };
};
@categoryCore Types Result <string, type ValidationError = {
field: string;
message: string;
}
ValidationError > => {
if (!email: string email .String.includes(searchString: string, position?: number): booleanReturns true if searchString appears as a substring of the result of converting this
object to a String, at one or more positions that are
greater than or equal to position; otherwise, returns false.
@paramsearchString search string@paramposition If position is undefined, 0 is assumed, so as to search all of the String. includes ('@')) {
return import Result Result .const fail: <{
readonly field: "email";
readonly message: "Invalid email format";
}>(error: {
readonly field: "email";
readonly message: "Invalid email format";
}) => Result.Result<never, {
readonly field: "email";
readonly message: "Invalid email format";
}> (+1 overload)
fail ({ field: "email" field : 'email', message: "Invalid email format" message : 'Invalid email format' });
}
return import Result Result .const succeed: <string>(value: string) => Result.Result<string, never> (+1 overload) succeed (email: string email );
};
const const result: Result.Result<string, ValidationError> result = const validateEmail: (email: string) => Result.Result<string, ValidationError> validateEmail ('test');
if (import Result Result .const isFailure: <ValidationError>(result: Result.Result<unknown, ValidationError>) => result is Result.Failure<ValidationError>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.@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, ValidationError> result )) {
// TypeScript knows result.error is ValidationError
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.log(message?: any, ...optionalParams: any[]): void (+1 overload)Prints to stdout 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 count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format() for more information.
@sincev0 .1.100 log (`Error in ${const result: Result.Failure<ValidationError> result .error: ValidationError error .field: string field }: ${const result: Result.Failure<ValidationError> result .error: ValidationError error .message: string message }`);
}#3. Composable
Results can be easily chained and composed (we'll cover this in later sections):
import { import Result Result } from '@praha/byethrow';
const const result: Result.ResultAsync<User, ValidationError | TransformError | SaveError> result = import Result Result .const pipe: <Result.Result<{
id: string;
name: string;
}, never>, Result.Result<User, ValidationError>, Result.Result<User, ValidationError | TransformError>, Result.ResultAsync<User, ValidationError | TransformError | SaveError>>(a: Result.Result<{
id: string;
name: string;
}, never>, ab: (a: Result.Result<{
id: string;
name: string;
}, never>) => Result.Result<User, ValidationError>, bc: (b: Result.Result<...>) => Result.Result<...>, cd: (c: Result.Result<...>) => Result.ResultAsync<...>) => Result.ResultAsync<...> (+25 overloads)
pipe (
import Result Result .const succeed: <{
id: string;
name: string;
}>(value: {
id: string;
name: string;
}) => Result.Result<{
id: string;
name: string;
}, never> (+1 overload)
succeed (const input: {
id: string;
name: string;
}
input ),
import Result Result .const andThen: <Result.Result<{
id: string;
name: string;
}, never>, Result.Result<User, ValidationError>>(fn: (a: {
id: string;
name: string;
}) => Result.Result<User, ValidationError>) => (result: Result.Result<{
id: string;
name: string;
}, never>) => Result.Result<User, ValidationError> (+1 overload)
andThen (const validate: (value: User) => Result.Result<User, ValidationError> validate ),
import Result Result .const andThen: <Result.Result<User, ValidationError>, Result.Result<User, TransformError>>(fn: (a: User) => Result.Result<User, TransformError>) => (result: Result.Result<User, ValidationError>) => Result.Result<User, ValidationError | TransformError> (+1 overload) andThen (const transform: (value: User) => Result.Result<User, TransformError> transform ),
import Result Result .const andThen: <Result.Result<User, ValidationError | TransformError>, Result.ResultAsync<User, SaveError>>(fn: (a: User) => Result.ResultAsync<User, SaveError>) => (result: Result.Result<User, ValidationError | TransformError>) => Result.ResultAsync<User, ValidationError | TransformError | SaveError> (+1 overload) andThen (const save: (value: User) => Result.ResultAsync<User, SaveError> save ),
);
// Type: Result.ResultAsync<User, ValidationError | TransformError | SaveError>#Async Results
@praha/byethrow also supports asynchronous operations.
Asynchronous Result is a type alias ResultAsync<T, E>, representing Promise<Result<T, E>>:
import { import Result Result } from '@praha/byethrow';
// ResultAsync is just Promise<Result<T, E>>
type type ResultAsync<T, E> = Promise<Result.Result<T, E>> ResultAsync <function (type parameter) T in type ResultAsync<T, E> T , function (type parameter) E in type ResultAsync<T, E> E > = interface Promise<T>Represents the completion of an asynchronous operation
Promise <import Result Result .type Result<T, E> = Result.Success<T> | Result.Failure<E>A union type representing either a success or a failure.
@typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@exampleimport { Result } from '@praha/byethrow';
const doSomething = (): Result.Result<number, string> => {
return Math.random() > 0.5
? { type: 'Success', value: 10 }
: { type: 'Failure', error: 'Oops' };
};
@categoryCore Types Result <function (type parameter) T in type ResultAsync<T, E> T , function (type parameter) E in type ResultAsync<T, E> E >>;
// The library handles both sync and async seamlessly
const const asyncResult: Result.ResultAsync<number, never> asyncResult = import Result Result .const succeed: <Promise<number>>(value: Promise<number>) => Result.ResultAsync<number, never> (+1 overload) succeed (var Promise: PromiseConstructorRepresents the completion of an asynchronous operation
Promise .PromiseConstructor.resolve<number>(value: number): Promise<number> (+2 overloads)Creates a new resolved promise for the provided value.
@paramvalue A promise.@returnsA promise whose internal state matches the provided promise. resolve (42));
// Type: Result.ResultAsync<number, never>
const const resolved: Result.Result<number, never> resolved = await const asyncResult: Result.ResultAsync<number, never> asyncResult ;
// Type: Result.Result<number, never>#Seamless Sync/Async Chaining
One of the powerful features of @praha/byethrow is that you can seamlessly chain synchronous and asynchronous Results together. When you mix sync and async operations in a pipeline, the result automatically becomes a ResultAsync:
import { import Result Result } from '@praha/byethrow';
// Sync function
const const validate: (input: string) => Result.Result<string, Error> validate = (input: string input : string): import Result Result .type Result<T, E> = Result.Success<T> | Result.Failure<E>A union type representing either a success or a failure.
@typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@exampleimport { Result } from '@praha/byethrow';
const doSomething = (): Result.Result<number, string> => {
return Math.random() > 0.5
? { type: 'Success', value: 10 }
: { type: 'Failure', error: 'Oops' };
};
@categoryCore Types Result <string, Error> => {
if (input: string input .String.length: numberReturns the length of a String object.
length === 0) {
return import Result Result .const fail: <Error>(error: Error) => Result.Result<never, Error> (+1 overload) fail (new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error ('Input is empty'));
}
return import Result Result .const succeed: <string>(value: string) => Result.Result<string, never> (+1 overload) succeed (input: string input );
};
// Async function
const const fetchData: (input: string) => Result.ResultAsync<number, Error> fetchData = async (input: string input : 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 <number, Error> => {
// Simulating an API call
return import Result Result .const succeed: <number>(value: number) => Result.Result<number, never> (+1 overload) succeed (input: string input .String.length: numberReturns the length of a String object.
length );
};
// Sync and async can be chained together seamlessly
const const result: Result.ResultAsync<number, Error> result = import Result Result .const pipe: <Result.Result<"hello", never>, Result.Result<string, Error>, Result.ResultAsync<number, Error>, Result.ResultAsync<number, Error>>(a: Result.Result<"hello", never>, ab: (a: Result.Result<"hello", never>) => Result.Result<string, Error>, bc: (b: Result.Result<string, Error>) => Result.ResultAsync<number, Error>, cd: (c: Result.ResultAsync<number, Error>) => Result.ResultAsync<number, Error>) => Result.ResultAsync<...> (+25 overloads) pipe (
import Result Result .const succeed: <"hello">(value: "hello") => Result.Result<"hello", never> (+1 overload) succeed ('hello'),
import Result Result .const andThen: <Result.Result<"hello", never>, Result.Result<string, Error>>(fn: (a: "hello") => Result.Result<string, Error>) => (result: Result.Result<"hello", never>) => Result.Result<string, Error> (+1 overload) andThen (const validate: (input: string) => Result.Result<string, Error> validate ), // sync
import Result Result .const andThen: <Result.Result<string, Error>, Result.ResultAsync<number, Error>>(fn: (a: string) => Result.ResultAsync<number, Error>) => (result: Result.Result<string, Error>) => Result.ResultAsync<number, Error> (+1 overload) andThen (const fetchData: (input: string) => Result.ResultAsync<number, Error> fetchData ), // async - from here, the pipeline becomes async
import Result Result .const andThen: <Result.ResultAsync<number, Error>, Result.Result<number, never>>(fn: (a: number) => Result.Result<number, never>) => (result: Result.ResultAsync<number, Error>) => Result.ResultAsync<number, Error> (+1 overload) andThen ((n: number n ) => import Result Result .const succeed: <number>(value: number) => Result.Result<number, never> (+1 overload) succeed (n: number n * 2)), // sync, but still in async context
);
// Type: Result.ResultAsync<number, Error>#References
| Function | Purpose |
|---|---|
| Success<T> | Represents a successful result |
| Failure<T> | Represents a failed result |
| Result<T, E> | A union type of Success or Failure |
| ResultAsync<T, E> | An asynchronous variant of Result |
