@jucie.io/result
v3.0.0
Published
A simple, powerful Result type for JavaScript with API serialization support
Maintainers
Readme
@jucie.io/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 @jucie.io/resultQuick Start
import { Result } from '@jucie.io/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 '@jucie.io/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 '@jucie.io/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 '@jucie.io/result';
const result = await fetchResult('/api/users/123');Option 3: Your own fetch, with Result.fromResponse()
const response = await fetch('/api/users/123');
const result = await Result.fromResponse(response);Result.parse(text) still works for a body you already hold as text, but it throws on anything that isn't JSON.
Why it works: Results serialize to JSON with a _result: true marker, and Result.fetch()/Result.parse() automatically restore all methods.
Result.fetch() resolves a Result for every answer, not only for servers that send Results:
- A body that is a Result is believed whatever the status, and comes back exactly as the server sent it. It gets no
data.status, nodata.body, and none of the options below. - Any other answer becomes a success when
response.ok. Otherwise it is a failure codedHTTP_<status>, and a 304 or an opaque (status 0) answer counts as a failure. - No answer at all, or a body that breaks off mid-read, is an error Result coded
NETWORK_ERROR.
It doesn't reject for a network or HTTP failure. A malformed init (an invalid header name, say) or an option callback that throws still rejects.
Calling a Third-Party API
Most APIs don't speak Result, but they all say whether it worked. Result.fetch() takes a third argument that says how to read their answers:
const result = await Result.fetch('https://api.github.com/user/repos', {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' },
}, {
// What a failure says. The default picks `message`, `error.message` or `error` from the body,
// then short text, then the status text.
reason: (body, response) => `GitHub refused (${response.status}): ${body?.message}`,
// Kept in the Result's data beside `status`, so whoever relays the failure knows whose it is.
data: () => ({ service: 'github' }),
});
if (Result.isFailure(result)) {
console.log(result.reason, result.data.status, result.data.body);
}value, reason, code and data each receive (body, response). The body is parsed JSON, else text, and null when empty. A long or HTML body is never used as the default reason (a proxy's error page is not a sentence), but it is always in a failure's data.body.
A Content-Type of application/json is added only when the body is a string and your headers don't set one, so a GET sends no preflight-triggering header and FormData keeps its boundary.
The network-error Result never carries your request's headers. Its data.url is the URL as a normalized string with any user:password removed (fetch refuses such a URL anyway, so it always ends as NETWORK_ERROR). A token in the query string is kept, so put secrets in headers.
If you already have a Response, use Result.fromResponse(response, options). It reads the body, so it throws a TypeError for a Response whose body was already read, or for anything that isn't a Response.
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 '@jucie.io/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: always resolves a Result (revived, or built from the status and body)
const result = await Result.fetch(url, init, { value, reason, code, data });
// An existing, unread Response as a Result
const result = await Result.fromResponse(response, { value, reason, code, data });
// 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 '@jucie.io/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 '@jucie.io/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 '@jucie.io/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 '@jucie.io/result';
// Explicit (self-documenting)
import { createSuccess, createFailure, createError } from '@jucie.io/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 | @jucie.io/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
126 tests passing. Run with:
npm testLicense
MIT License with Commons Clause - See LICENSE file for details. Versions up to 2.0.2 were published under ISC.
Contributing
Contributions welcome! Please feel free to submit a Pull Request.
