@brickworks/result
v2.0.2
Published
A simple, powerful Result type for JavaScript with API serialization support
Maintainers
Readme
@brickworks/result
A simple, powerful Result type for JavaScript with built-in API serialization support.
Features
- 🚀 Zero Dependencies - Lightweight and fast
- 🧹 Simple - No symbols, no hidden properties, just plain objects
- 🎯 Three States - Success/Failure/Error (not just Ok/Err)
- 🔗 Chainable - Fluent API with
.onSuccess()/.onFailure()/.onError() - 🌐 API-Ready - Built-in serialization with
Result.fetch()andResult.parse() - ⚡ Async Support -
fromPromise(),all(), and more - ✍️ Flexible Naming - Use
success()orcreateSuccess(), your choice - 📝 TypeScript - Full type definitions included
Installation
npm install @brickworks/resultQuick Start
import { Result } from '@brickworks/result';
// Create results
const ok = Result.success({ id: 123, name: 'Alice' });
const fail = Result.failure('User not found');
// Chain handlers
ok
.onSuccess(r => console.log('User:', r.value))
.onFailure(r => console.error('Failed:', r.reason));
// Transform
const greeting = Result.map(ok, user => `Hello ${user.name}!`);
// Pattern match
Result.match(ok, {
success: r => console.log(r.value),
failure: r => console.error(r.reason),
error: e => console.error(e.message)
});API Usage (The Killer Feature)
Server Side
import express from 'express';
import { Result } from '@brickworks/result';
const app = express();
Result.setThrowErrors(false);
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (user) {
res.json(Result.success(user));
} else {
res.status(404).json(Result.failure('User not found'));
}
});Client Side
Option 1: Using Result.fetch() (simplest)
import { Result } from '@brickworks/result';
// One line - fetch and parse automatically
const result = await Result.fetch('/api/users/123');
result
.onSuccess(r => displayUser(r.value))
.onFailure(r => showError(r.reason))
.onError(e => logError(e.message));Option 2: Using fetchResult() (named import)
import { fetchResult } from '@brickworks/result';
const result = await fetchResult('/api/users/123');Option 3: Manual fetch with Result.parse()
const response = await fetch('/api/users/123');
const result = Result.parse(await response.text());Why it works: Results serialize to JSON with a _result: true marker, and Result.fetch()/Result.parse() automatically restore all methods.
Core Concepts
Three States
| State | Use Case | Example | |-------|----------|---------| | Success | Operation succeeded | User found, data saved | | Failure | Expected business failure | Validation failed, not found | | Error | Unexpected exception | DB crash, network timeout |
import { Result } from '@brickworks/result';
// Success - when things work
const ok = Result.success({ userId: 123 });
// Failure - expected problems (validation, not found, etc.)
const fail = Result.failure('Invalid email', { field: 'email' }, 'VALIDATION_ERROR');
// Error - unexpected problems (system errors, crashes)
Result.setThrowErrors(false); // Don't throw, return error object
const err = Result.error('Database connection failed', { host: 'localhost' });API Reference
Creating Results
// All three methods support: (value/reason/message, data?, code?)
Result.success(data)
Result.failure(reason)
Result.error(message) // Set Result.setThrowErrors(false) first
// Also available with explicit names:
Result.createSuccess(data)
Result.createFailure(reason)
Result.createError(message)Fetching & Parsing (API Helpers)
// Fetch and auto-parse Result from API
const result = await Result.fetch(url, options);
// Parse JSON string to Result
const result = Result.parse(jsonString);
// Traditional approach (still works)
const result = JSON.parse(jsonString, Result.revive);Guards
Result.isResult(value) // → true if any Result type
Result.isSuccess(result) // → true if success
Result.isFailure(result) // → true if failure
Result.isError(result) // → true if error
// Aliases
Result.successful(result)
Result.failed(result)
Result.errored(result)Extractors
Result.getValue(result) // → value (throws if not success)
Result.getReason(result) // → reason (throws if not failure)
Result.getData(result) // → data object (always safe)Chainable Methods
result
.onSuccess(r => console.log('Value:', r.value))
.onFailure(r => console.error('Reason:', r.reason))
.onError(e => console.error('Error:', e.message));
// Returns the same result for chainingTransformations
// map - transform success value
const doubled = Result.map(result, val => val * 2);
// mapFailure - transform failure reason
const mapped = Result.mapFailure(result, reason => reason.toUpperCase());
// flatMap - chain operations that return Results
const chained = Result.flatMap(result, val => getUserById(val));
// match - pattern matching (returns value)
const output = Result.match(result, {
success: r => r.value,
failure: r => 'default',
error: e => null
});
// unwrapOr - extract with fallback
const value = Result.unwrapOr(result, 'default');
// unwrapOrElse - extract with computed fallback
const value = Result.unwrapOrElse(result, r => computeDefault(r));Async Operations
// Convert Promise to Result
const result = await Result.fromPromise(
fetch('/api/users'),
{
failureCode: 'FETCH_FAILED',
mapError: e => ({ reason: e.message, data: {} })
}
);
// Combine multiple Results (short-circuits on first failure)
const results = await Result.all([
Result.fetch('/api/users/1'),
Result.fetch('/api/users/2'),
Result.fetch('/api/users/3')
]);Wire Format
// Serialize (happens automatically with res.json())
const json = JSON.stringify(result);
// → {"kind":"success","ok":true,"value":...,"_result":true}
// Deserialize
const result = Result.parse(json);
// or: JSON.parse(json, Result.revive)
// Manual conversion
const wire = Result.toWire(result);
const result = Result.fromWire(wire);Practical Examples
API Helper
import { Result } from '@brickworks/result';
export const api = {
getUser: (id) => Result.fetch(`/api/users/${id}`),
createUser: (data) => Result.fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
}),
updateUser: (id, data) => Result.fetch(`/api/users/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
})
};
// Usage
const userResult = await api.getUser(123);
userResult.onSuccess(r => displayUser(r.value));Form Validation
function validateEmail(email) {
return email.includes('@')
? Result.success(email)
: Result.failure('Invalid email', { field: 'email' });
}
function validatePassword(password) {
return password.length >= 8
? Result.success(password)
: Result.failure('Password too short', { field: 'password' });
}
function validateForm(data) {
const emailResult = validateEmail(data.email);
const passwordResult = validatePassword(data.password);
return Result.flatMap(emailResult, email =>
Result.map(passwordResult, password =>
({ email, password })
)
);
}React Hook
import { useState, useEffect } from 'react';
import { Result } from '@brickworks/result';
function useApi(url) {
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
Result.fetch(url).then(r => {
setResult(r);
setLoading(false);
});
}, [url]);
return { result, loading };
}
// Use it
function UserProfile({ userId }) {
const { result, loading } = useApi(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
return Result.match(result, {
success: r => <div>Welcome {r.value.name}!</div>,
failure: r => <div>Error: {r.reason}</div>,
error: e => <div>System error</div>
});
}Error Handling with Codes
const result = await api.login(email, password);
result
.onSuccess(r => {
localStorage.setItem('token', r.value.token);
navigate('/dashboard');
})
.onFailure(r => {
switch (r.code) {
case 'VALIDATION_ERROR':
highlightField(r.data.field);
break;
case 'AUTH_FAILED':
showError('Invalid credentials');
break;
case 'USER_NOT_FOUND':
showError('User not found');
break;
}
})
.onError(e => {
logToSentry(e);
showGenericError();
});Configuration
// Control error throwing (default: true)
Result.setThrowErrors(false); // Don't throw, return error objects
// Logging
Result.enableLog();
Result.setLogLevel('debug'); // 'error' | 'warn' | 'info' | 'debug'
Result.setMaxLog(1000);
Result.printLog();
Result.clearLog();TypeScript
Full type definitions included:
import { Result, SuccessResult, FailureResult, ErrorResult } from '@brickworks/result';
async function getUser(id: number): Promise<Result<User>> {
return Result.fetch<User>(`/api/users/${id}`);
}
const result = await getUser(123);
if (Result.isSuccess(result)) {
result.value.name; // TypeScript knows the type!
}Naming Options
The library supports both terse and explicit naming:
// Terse (common in FP)
import { success, failure, error } from '@brickworks/result';
// Explicit (self-documenting)
import { createSuccess, createFailure, createError } from '@brickworks/result';
// Both styles work the same!Why This Library?
vs. Traditional Error Handling
// ❌ Traditional (throw/catch)
try {
const response = await fetch('/api/users/1');
const data = await response.json();
console.log(data);
} catch (err) {
console.error(err); // Lost context!
}
// ✅ Result (explicit handling)
const result = await Result.fetch('/api/users/1');
result
.onSuccess(r => console.log(r.value))
.onFailure(r => showError(r.reason, r.code)) // Rich context
.onError(e => logToSentry(e)); // Type-safevs. Other Result Libraries
| Feature | @brickworks/result | Others |
|---------|---------------|--------|
| Three states | Success/Failure/Error | Usually just Ok/Err |
| API serialization | Built-in | Manual |
| Fetch wrapper | Result.fetch() | ❌ |
| Parse helper | Result.parse() | ❌ |
| Chainable methods | .onSuccess() etc. | Varies |
| Rich metadata | codes, timestamps, data | Limited |
| Dependencies | Zero | Varies |
Tests
95 tests passing. Run with:
npm testLicense
ISC
Contributing
Contributions welcome! Please feel free to submit a Pull Request.
