Best Practices
10 core principles for effective Result.js usage.
1. Define Explicit Error Types
Avoid generic errors. Define domain-specific error types:
typescript
// ✗ Too generic
function process(data: Data): Result<Output, Error>
// ✓ Better: specific error type
type ApiError =
| { type: 'not_found'; resource: string }
| { type: 'validation'; field: string; message: string }
| { type: 'unauthorized' }
| { type: 'server_error'; status: number }
function process(data: Data): Result<Output, ApiError>Benefits: Typescript enforces handling all cases, enables precise recovery, self-documenting code.
2. Prefer Chaining Over Nested Checks
typescript
// ✓ Prefer: clean pipeline
Result.ok(input)
.andThen(validate)
.andThen(transform)
.andThen(save)
// ✗ Avoid: nested checks
const validated = validate(input)
if (validated.isErr()) return validated
const transformed = transform(validated.unwrap())
if (transformed.isErr()) return transformedIf chains become too long, break into smaller functions.
3. Use Pattern Matching for Final Handling
typescript
// ✓ Prefer: explicit intent
result.match({
ok: (value) => processValue(value),
err: (error) => handleError(error)
})
// ✗ Avoid: if-else chains
if (result.isOk()) {
processValue(result.unwrap())
} else {
handleError(result.unwrapErr())
}Perfect for React components and HTTP responses.
4. Transform Errors with Context
typescript
// ✗ Generic error loses context
fetchUser(id).mapErr(e => new Error('Failed'))
// ✓ Better: Add context
fetchUser(id).mapErr(err => ({
...err,
context: { userId: id, timestamp: Date.now() }
}))
// ✓ Standard structure
fetchUser(id).mapErr(err => ({
type: 'api_error',
status: err.statusCode || 500,
message: err.message || 'Unknown error',
userId: id,
timestamp: Date.now()
}))5. Use Inspect for Side Effects
Never put side effects in map():
typescript
// ✗ Avoid: side effects in map
result.map(x => {
console.log(x)
saveToDb(x)
return x
})
// ✓ Prefer: explicit intent
result
.inspect(x => console.log('Received:', x))
.inspectErr(e => logger.error('Failed:', e))
.andThen(saveToDb)6. Be Consistent with Error Handling
typescript
// ✓ Good: consistent use of Result
function validateEmail(email: string): Result<string, ValidationError> { ... }
function validatePassword(password: string): Result<string, ValidationError> { ... }
// ✗ Bad: mixing patterns
function validateEmail(email: string): Result<string, Error> { ... }
function validatePassword(password: string): Promise<string> { ... }
function validateAge(age: number): boolean { ... }7. Fail-Fast vs Error Accumulation
Choose based on dependencies:
typescript
// Fail-fast: later steps depend on earlier ones
Result.all([
validateStructure(data),
validateReferences(data),
validateBusiness(data)
])
// Collect all: independent validations
const errors: ValidationError[] = []
if (validateEmail(data).isErr()) errors.push(...)
if (validatePassword(data).isErr()) errors.push(...)
return errors.length > 0 ? Result.err(errors) : Result.ok(...)8. Always Await Async Methods
typescript
// ✗ Wrong: forgot await
const result = operation().mapAsync(async x => x * 2)
console.log(result.unwrap()) // Error: result is Promise!
// ✓ Correct
const result = await operation().mapAsync(async x => x * 2)
console.log(result.unwrap())9. Log Errors Meaningfully
typescript
// ✗ Avoid: silent failures
operation().orElse(() => Result.ok(defaultValue))
// ✓ Better: log failures
operation()
.inspectErr(error => {
logger.error('Operation failed:', {
error,
timestamp: Date.now(),
context: { userId, action }
})
})
.orElse(() => Result.ok(defaultValue))10. Test Both Paths
Always test success and failure cases:
typescript
describe('divide', () => {
it('should return Ok for valid division', () => {
const result = divide(10, 2)
expect(result.isOk()).toBe(true)
expect(result.unwrap()).toBe(5)
})
it('should return Err for division by zero', () => {
const result = divide(10, 0)
expect(result.isErr()).toBe(true)
expect(result.unwrapErr()).toBe('Division by zero')
})
})Anti-Patterns to Avoid
Mixing Result with Exceptions
typescript
// ✗ Inconsistent error handling
function process(data: Data): Result<Output, Error> {
const result = validate(data)
if (result.isErr()) return result
if (someCondition) {
throw new Error('Bad!') // Mixing patterns!
}
return Result.ok(output)
}
// ✓ Consistent
function process(data: Data): Result<Output, Error> {
const result = validate(data)
if (result.isErr()) return result
if (someCondition) {
return Result.err(new Error('Bad!'))
}
return Result.ok(output)
}Overusing Result
typescript
// ✗ Avoid: Result for operations that can't fail
function add(a: number, b: number): Result<number, never> {
return Result.ok(a + b)
}
// ✓ Good: Result for fallible operations
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return Result.err('Division by zero')
return Result.ok(a / b)
}Summary Checklist
- ✓ Define explicit error types
- ✓ Chain operations cleanly
- ✓ Use pattern matching for final handling
- ✓ Transform errors with context
- ✓ Use inspect() for side effects
- ✓ Be consistent with error handling
- ✓ Choose fail-fast or collect all appropriately
- ✓ Always await async methods
- ✓ Log errors meaningfully
- ✓ Test both success and failure paths
Next Steps
- Migration Guide — Adopt Result in existing code
- Troubleshooting — Common issues