Result Pattern API
The Result pattern is Kairo's core error handling mechanism. All operations return Result<Error, Data> types for safe, explicit error handling.
Type Definition
type Result<E, T> = OkResult<T> | ErrResult<E>
interface OkResult<T> {
tag: 'Ok'
value: T
}
interface ErrResult<E> {
tag: 'Err'
error: E
}Creation Functions
Result.Ok<T>(value: T): OkResult<T>
Creates a successful result containing a value.
const success = Result.Ok(42)
const userResult = Result.Ok({ id: '1', name: 'John' })
const listResult = Result.Ok([1, 2, 3])Result.Err<E>(error: E): ErrResult<E>
Creates an error result containing an error.
const error = Result.Err('Something went wrong')
const detailedError = Result.Err({
code: 'VALIDATION_ERROR',
message: 'Invalid input',
field: 'email'
})Result.fromPromise<T>(promise: Promise<T>, errorMapper?: (error: unknown) => E): Promise<Result<E, T>>
Converts a Promise to a Result, catching any thrown exceptions.
// Basic usage
const result = await Result.fromPromise(
fetch('/api/users').then(r => r.json())
)
// With error mapping
const result = await Result.fromPromise(
fetch('/api/users').then(r => r.json()),
(error) => ({
code: 'FETCH_ERROR',
message: error.message,
timestamp: Date.now()
})
)Result.fromTry<T>(fn: () => T, errorMapper?: (error: unknown) => E): Result<E, T>
Executes a function and catches any thrown exceptions, returning a Result.
// Basic usage
const result = Result.fromTry(() => JSON.parse(jsonString))
// With error mapping
const result = Result.fromTry(
() => JSON.parse(jsonString),
(error) => ({
code: 'PARSE_ERROR',
message: 'Invalid JSON',
cause: error
})
)Type Guards
Result.isOk<T>(result: Result<any, T>): result is OkResult<T>
Checks if a Result is successful and narrows the type.
const result = await service.get('/api/users')
if (Result.isOk(result)) {
// result.value is properly typed
console.log('Users:', result.value)
result.value.forEach(user => console.log(user.name))
}Result.isErr<E>(result: Result<E, any>): result is ErrResult<E>
Checks if a Result is an error and narrows the type.
const result = await service.get('/api/users')
if (Result.isErr(result)) {
// result.error is properly typed
console.error('Error:', result.error.message)
console.error('Code:', result.error.code)
}Pattern Matching
Result.match<E, T, U>(result: Result<E, T>, handlers: MatchHandlers<E, T, U>): U
Pattern matching for Results with exhaustive case handling.
interface MatchHandlers<E, T, U> {
Ok: (value: T) => U
Err: (error: E) => U
}
// Basic usage
const message = Result.match(result, {
Ok: users => `Found ${users.length} users`,
Err: error => `Error: ${error.message}`
})
// With complex handling
const processedData = Result.match(userResult, {
Ok: users => {
const active = users.filter(u => u.active)
return {
success: true,
data: active,
count: active.length
}
},
Err: error => {
logger.error('User fetch failed', error)
return {
success: false,
data: [],
count: 0
}
}
})Transformation Functions
Result.map<E, T, U>(result: Result<E, T>, fn: (value: T) => U): Result<E, U>
Transforms the success value of a Result, leaving errors unchanged.
const numberResult = Result.Ok(42)
const stringResult = Result.map(numberResult, n => n.toString())
// Result<never, string>
const userResult = await service.get('/api/users')
const nameResult = Result.map(userResult, users =>
users.map(user => user.name)
)
// Result<ServiceError, string[]>Result.mapError<E, T, F>(result: Result<E, T>, fn: (error: E) => F): Result<F, T>
Transforms the error of a Result, leaving success values unchanged.
const result = await service.get('/api/users')
const enhancedResult = Result.mapError(result, error => ({
...error,
timestamp: Date.now(),
context: 'user-fetch',
userMessage: 'Failed to load users'
}))Result.flatMap<E, T, U>(result: Result<E, T>, fn: (value: T) => Result<E, U>): Result<E, U>
Chains operations that return Results, flattening nested Results.
const userResult = await service.get('/api/users')
const processedResult = Result.flatMap(userResult, users => {
if (users.length === 0) {
return Result.Err({
code: 'NO_USERS',
message: 'No users found'
})
}
const active = users.filter(u => u.active)
return Result.Ok(active)
})Utility Functions
Result.unwrap<T>(result: Result<any, T>): T
Extracts the value from a successful Result. Throws if the Result is an error.
const result = Result.Ok(42)
const value = Result.unwrap(result) // 42
// ⚠️ Dangerous - will throw if error
const errorResult = Result.Err('Failed')
const value = Result.unwrap(errorResult) // Throws!Result.unwrapOr<T>(result: Result<any, T>, defaultValue: T): T
Extracts the value from a Result, returning a default if it's an error.
const successResult = Result.Ok(42)
const value1 = Result.unwrapOr(successResult, 0) // 42
const errorResult = Result.Err('Failed')
const value2 = Result.unwrapOr(errorResult, 0) // 0Result.unwrapOrElse<T>(result: Result<any, T>, fn: (error: any) => T): T
Extracts the value from a Result, computing a default if it's an error.
const result = Result.Err({ code: 'NOT_FOUND' })
const value = Result.unwrapOrElse(result, error => {
console.log('Error occurred:', error.code)
return getDefaultValue()
})Combining Results
Multiple Results
// Combine multiple Results
const combineResults = <T>(results: Result<any, T>[]): Result<any, T[]> => {
const values: T[] = []
for (const result of results) {
if (Result.isErr(result)) {
return result
}
values.push(result.value)
}
return Result.Ok(values)
}
// Usage
const results = await Promise.all([
service.get('/api/users'),
service.get('/api/posts'),
service.get('/api/comments')
])
const combined = combineResults(results)
if (Result.isOk(combined)) {
const [users, posts, comments] = combined.value
}Sequential Operations
const processUserData = async (userId: string) => {
// Fetch user
const userResult = await service.get(`/api/users/${userId}`)
if (Result.isErr(userResult)) {
return userResult
}
// Validate user
const validationResult = data.validate(userResult.value, UserSchema)
if (Result.isErr(validationResult)) {
return validationResult
}
// Process user
const processedResult = pipeline.map([validationResult.value], processUser)
if (Result.isErr(processedResult)) {
return processedResult
}
return Result.Ok(processedResult.value[0])
}Advanced Patterns
Result Monad
class ResultMonad<E, T> {
constructor(private result: Result<E, T>) {}
static of<T>(value: T): ResultMonad<never, T> {
return new ResultMonad(Result.Ok(value))
}
static error<E>(error: E): ResultMonad<E, never> {
return new ResultMonad(Result.Err(error))
}
map<U>(fn: (value: T) => U): ResultMonad<E, U> {
return new ResultMonad(Result.map(this.result, fn))
}
flatMap<U>(fn: (value: T) => ResultMonad<E, U>): ResultMonad<E, U> {
if (Result.isErr(this.result)) {
return new ResultMonad(this.result)
}
return fn(this.result.value)
}
mapError<F>(fn: (error: E) => F): ResultMonad<F, T> {
return new ResultMonad(Result.mapError(this.result, fn))
}
unwrap(): Result<E, T> {
return this.result
}
}
// Usage
const result = ResultMonad.of(42)
.map(x => x * 2)
.flatMap(x => x > 50 ?
ResultMonad.of(x) :
ResultMonad.error('Too small')
)
.unwrap()Async Result Operations
const asyncMap = async <E, T, U>(
result: Result<E, T>,
fn: (value: T) => Promise<U>
): Promise<Result<E, U>> => {
if (Result.isErr(result)) {
return result
}
try {
const value = await fn(result.value)
return Result.Ok(value)
} catch (error) {
return Result.Err(error as E)
}
}
// Usage
const userResult = await service.get('/api/users')
const processedResult = await asyncMap(userResult, async users => {
const processed = await Promise.all(
users.map(user => processUserAsync(user))
)
return processed
})Result Collection
const collect = <E, T>(results: Result<E, T>[]): Result<E[], T[]> => {
const values: T[] = []
const errors: E[] = []
for (const result of results) {
if (Result.isOk(result)) {
values.push(result.value)
} else {
errors.push(result.error)
}
}
if (errors.length > 0) {
return Result.Err(errors)
}
return Result.Ok(values)
}
// Usage with partial failures
const results = await Promise.all([
service.get('/api/users'),
service.get('/api/posts'),
service.get('/api/comments')
])
const collected = collect(results)
Result.match(collected, {
Ok: ([users, posts, comments]) => {
// All successful
},
Err: errors => {
// Some failed
console.log(`${errors.length} requests failed`)
}
})Testing with Results
Unit Tests
describe('Result API', () => {
it('should create Ok result', () => {
const result = Result.Ok(42)
expect(Result.isOk(result)).toBe(true)
expect(Result.isErr(result)).toBe(false)
if (Result.isOk(result)) {
expect(result.value).toBe(42)
}
})
it('should create Err result', () => {
const error = { code: 'TEST_ERROR', message: 'Test failed' }
const result = Result.Err(error)
expect(Result.isErr(result)).toBe(true)
expect(Result.isOk(result)).toBe(false)
if (Result.isErr(result)) {
expect(result.error).toEqual(error)
}
})
it('should map Ok values', () => {
const result = Result.Ok(10)
const mapped = Result.map(result, x => x * 2)
expect(Result.isOk(mapped)).toBe(true)
if (Result.isOk(mapped)) {
expect(mapped.value).toBe(20)
}
})
it('should not map Err values', () => {
const result = Result.Err('error')
const mapped = Result.map(result, x => x * 2)
expect(Result.isErr(mapped)).toBe(true)
if (Result.isErr(mapped)) {
expect(mapped.error).toBe('error')
}
})
})Integration Tests
describe('Result Pattern Integration', () => {
it('should handle service operations', async () => {
const result = await service.get('/api/users')
expect(result).toHaveProperty('tag')
expect(['Ok', 'Err']).toContain(result.tag)
if (Result.isOk(result)) {
expect(Array.isArray(result.value)).toBe(true)
}
})
it('should chain operations correctly', async () => {
const result = await service.get('/api/users')
const processed = Result.flatMap(result, users => {
if (users.length === 0) {
return Result.Err({ code: 'NO_USERS' })
}
return Result.Ok(users.filter(u => u.active))
})
// Test both success and error paths
if (Result.isOk(processed)) {
expect(Array.isArray(processed.value)).toBe(true)
} else {
expect(processed.error).toHaveProperty('code')
}
})
})Performance Considerations
Memory Usage
// ✅ Efficient - reuse Results
const cachedResult = Result.Ok(expensiveComputation())
const result1 = Result.map(cachedResult, transform1)
const result2 = Result.map(cachedResult, transform2)
// ❌ Inefficient - repeated computation
const result1 = Result.map(Result.Ok(expensiveComputation()), transform1)
const result2 = Result.map(Result.Ok(expensiveComputation()), transform2)Error Handling Performance
// ✅ Efficient - early returns
const processData = (data: any[]) => {
if (data.length === 0) {
return Result.Err({ code: 'EMPTY_DATA' })
}
// Process data...
return Result.Ok(processed)
}
// ❌ Less efficient - unnecessary processing
const processData = (data: any[]) => {
const processed = expensiveOperation(data)
if (data.length === 0) {
return Result.Err({ code: 'EMPTY_DATA' })
}
return Result.Ok(processed)
}Best Practices
- Always use type guards - Check Result type before accessing values
- Use pattern matching - For complex Result handling
- Avoid unwrap() - Use unwrapOr() or unwrapOrElse() for safety
- Chain operations - Use flatMap for sequential operations
- Handle errors explicitly - Don't ignore error cases
- Provide context - Include relevant information in errors
- Use early returns - Simplify control flow with early error returns
Next Steps
- Error Handling Guide - Comprehensive error management
- Service API - SERVICE pillar documentation
- Data API - DATA pillar documentation
- Pipeline API - PIPELINE pillar documentation