Result
Type-safe error handling without exceptions
The Result type is a type-safe way to handle operations that can fail without throwing exceptions. It represents either a successful operation with a value, or a failed operation with an error.
Why Use Result?
Traditional error handling in JavaScript relies on exceptions and try-catch blocks. This approach has several problems:
- Type safety: Exceptions break type safety - you can't know from function signatures what errors might be thrown
- Hidden control flow: Exceptions create invisible goto statements that jump to catch blocks
- Forced error handling: With Result, errors are explicit and must be handled
Result solves these problems by making errors explicit in the type system and forcing you to handle them.
Basic Usage
Creating Results
Use the Result namespace constructors:
import { Result } from '@deessejs/functions';
// Success case
const result1 = Result.success(42);
console.log(result1.value); // 42
// Failure case
const result2 = Result.failure(new Error("Something went wrong"));
console.log(result2.error.message); // "Something went wrong"Type Guards
Use isSuccess() and isFailure() to check the result and narrow types:
function divide(a: number, b: number): Result<number, Error> {
if (b === 0) {
return Result.failure(new Error("Division by zero"));
}
return Result.success(a / b);
}
const result = divide(10, 2);
if (result.isSuccess()) {
// TypeScript knows result is Success here
console.log("Result:", result.value);
} else {
// TypeScript knows result is Failure here
console.error("Error:", result.error.message);
}Pattern Matching
The match() method provides elegant pattern matching:
const message = result.match({
onSuccess: (value) => `Success! Got: ${value}`,
onFailure: (error) => `Error: ${error.message}`,
});
console.log(message);Type Safety
Result provides excellent type safety with TypeScript:
function fetchUser(id: string): Result<User, Error> {
// ... implementation
}
const result = fetchUser("123");
if (result.isSuccess()) {
// TypeScript knows:
// - result.value exists and is of type User
// - result.error does NOT exist
console.log(result.value.name);
}Error Types
You can customize the error type:
// Using custom error type
type ValidationError = { field: string; message: string };
function validateEmail(email: string): Result<string, ValidationError> {
if (!email.includes('@')) {
return Result.failure({
field: 'email',
message: 'Invalid email format',
});
}
return Result.success(email);
}Best Practices
1. Always Handle Both Cases
Use match() to ensure both success and failure are handled:
// ✅ Good - explicit handling
result.match({
onSuccess: (value) => console.log(value),
onFailure: (error) => console.error(error),
});
// ⚠️ Be careful - you might forget to handle failure
if (result.isSuccess()) {
console.log(result.value);
}2. Don't Nest Results
Avoid nested Results - use flat composition:
// ❌ Bad - nested Results
function bad(): Result<Result<number, Error>, Error> {
// ...
}
// ✅ Good - flat Result
function good(): Result<number, Error> {
// ...
}3. Use Descriptive Error Types
Custom error types make error handling more precise:
type ApiError =
| { kind: 'network'; message: string }
| { kind: 'validation'; field: string }
| { kind: 'auth'; reason: string };
function fetchData(): Result<Data, ApiError> {
// ...
}See Also
- Maybe - For optional values instead of null/undefined
- Try - For wrapping code that might throw exceptions
- Outcome - Advanced result system with causes and exceptions
- AsyncResult - Async version of Result