💡 摘要
中文总结。
🎯 适合人群
🤖 AI 吐槽: “看起来很能打,但别让配置把人劝退。”
风险:Low。建议检查:是否执行 shell/命令行指令;是否发起外网请求(SSRF/数据外发);文件读写范围与路径穿越风险;依赖锁定与供应链风险。以最小权限运行,并在生产环境启用前审计代码与依赖。
better-result
Lightweight Result type for TypeScript with generator-based composition.
Install
New to better-result?
npx better-result init
Upgrading from v1?
npx better-result migrate
Quick Start
import { Result } from "better-result"; // Wrap throwing functions const parsed = Result.try(() => JSON.parse(input)); // Check and use if (Result.isOk(parsed)) { console.log(parsed.value); } else { console.error(parsed.error); } // Or use pattern matching const message = parsed.match({ ok: (data) => `Got: ${data.name}`, err: (e) => `Failed: ${e.message}`, });
Contents
- Creating Results
- Transforming Results
- Handling Errors
- Extracting Values
- Generator Composition
- Retry Support
- UnhandledException
- Panic
- Tagged Errors
- Serialization
- API Reference
- Agents & AI
Creating Results
// Success const ok = Result.ok(42); // Error const err = Result.err(new Error("failed")); // From throwing function const result = Result.try(() => riskyOperation()); // From promise const result = await Result.tryPromise(() => fetch(url)); // With custom error handling const result = Result.try({ try: () => JSON.parse(input), catch: (e) => new ParseError(e), });
Transforming Results
const result = Result.ok(2) .map((x) => x * 2) // Ok(4) .andThen( ( x, // Chain Result-returning functions ) => (x > 0 ? Result.ok(x) : Result.err("negative")), ); // Standalone functions (data-first or data-last) Result.map(result, (x) => x + 1); Result.map((x) => x + 1)(result); // Pipeable
Handling Errors
// Transform error type const result = fetchUser(id).mapError( (e) => new AppError(`Failed to fetch user: ${e.message}`), ); // Recover from specific errors const result = fetchUser(id).match({ ok: (user) => Result.ok(user), err: (e) => e._tag === "NotFoundError" ? Result.ok(defaultUser) : Result.err(e), });
Extracting Values
// Unwrap (throws on Err) const value = result.unwrap(); const value = result.unwrap("custom error message"); // With fallback const value = result.unwrapOr(defaultValue); // Pattern match const value = result.match({ ok: (v) => v, err: (e) => fallback, });
Generator Composition
Chain multiple Results without nested callbacks or early returns:
const result = Result.gen(function* () { const a = yield* parseNumber(inputA); // Unwraps or short-circuits const b = yield* parseNumber(inputB); const c = yield* divide(a, b); return Result.ok(c); }); // Result<number, ParseError | DivisionError>
Async version with Result.await:
const result = await Result.gen(async function* () { const user = yield* Result.await(fetchUser(id)); const posts = yield* Result.await(fetchPosts(user.id)); return Result.ok({ user, posts }); });
Errors from all yielded Results are automatically collected into the final error union type.
Normalizing Error Types
Use mapError on the output of Result.gen() to unify multiple error types into a single type:
class ParseError extends TaggedError("ParseError")<{ message: string }>() {} class ValidationError extends TaggedError("ValidationError")<{ message: string }>() {} class AppError extends TaggedError("AppError")<{ source: string; message: string }>() {} const result = Result.gen(function* () { const parsed = yield* parseInput(input); // Err: ParseError const valid = yield* validate(parsed); // Err: ValidationError return Result.ok(valid); }).mapError((e): AppError => new AppError({ source: e._tag, message: e.message })); // Result<ValidatedData, AppError> - error union normalized to single type
Retry Support
const result = await Result.tryPromise(() => fetch(url), { retry: { times: 3, delayMs: 100, backoff: "exponential", // or "linear" | "constant" }, });
Conditional Retry
Retry only for specific error types using shouldRetry:
class NetworkError extends TaggedError("NetworkError")<{ message: string }>() {} class ValidationError extends TaggedError("ValidationError")<{ message: string }>() {} const result = await Result.tryPromise( { try: () => fetchData(url), catch: (e) => e instanceof TypeError // Network failures often throw TypeError ? new NetworkError({ message: (e as Error).message }) : new ValidationError({ message: String(e) }), }, { retry: { times: 3, delayMs: 100, backoff: "exponential", shouldRetry: (e) => e._tag === "NetworkError", // Only retry network errors }, }, );
Async Retry Decisions
For retry decisions that require async operations (rate limits, feature flags, etc.), enrich the error in the catch handler instead of making shouldRetry async:
class ApiError extends TaggedError("ApiError")<{ message: string; rateLimited: boolean; }>() {} const result = await Result.tryPromise( { try: () => callApi(url), catch: async (e) => { // Fetch async state in catch handler const retryAfter = await redis.get(`ratelimit:${userId}`); return new ApiError({ message: (e as Error).message, rateLimited: retryAfter !== null, }); }, }, { retry: { times: 3, delayMs: 100, backoff: "exponential", shouldRetry: (e) => !e.rateLimited, // Sync predicate uses enriched error }, }, );
UnhandledException
When Result.try() or Result.tryPromise() catches an exception without a custom handler, the error type is UnhandledException:
import { Result, UnhandledException } from "better-result"; // Automatic — error type is UnhandledException const result = Result.try(() => JSON.parse(input)); // ^? Result<unknown, UnhandledException> // Custom handler — you control the error type const result = Result.try({ try: () => JSON.parse(input), catch: (e) => new ParseError(e), }); // ^? Result<unknown, ParseError> // Same for async await Result.tryPromise(() => fetch(url)); // ^? Promise<Result<Response, UnhandledException>>
Access the original exception via .cause:
if (Result.isError(result)) { const original = result.error.cause; if (original instanceof SyntaxError) { // Handle JSON parse error } }
Panic
Thrown (not returned) when user callbacks throw inside Result operations. Represents a defect in your code, not a domain error.
import { Panic } from "better-result"; // Callback throws → Panic Result.ok(1).map(() => { throw new Error("bug"); }); // throws Panic // Generator cleanup throws → Panic Result.gen(function* () { try { yield* Result.err("expected failure"); } finally { throw new Error("cleanup bug"); } }); // throws Panic // Catch handler throws → Panic Result.try({ try: () => riskyOp(), catch: () => { throw new Error("bug in handler"); }, }); // throws Panic
Why Panic? Err is for recoverable domain errors. Panic is for bugs — like Rust's panic!(). If your .map() callback throws, that's not an error to handle, it's a defect to fix. Returning Err would collapse type safety (Result<T, E> becomes Result<T, E | unknown>).
Panic properties:
| Property | Type | Description |
| --------- | --------- | ----------------------------------- |
| message | string | Describes where/what panicked |
| cause | unknown | The exception that was thrown |
Panic also provides toJSON() for error reporting services (Sentry, etc.).
Tagged Errors
Build exhaustive error handling with discriminated unions:
import { TaggedError, matchError, matchErrorPartial } from "better-result"; // Factory API: TaggedError("Tag")<Props>() class NotFoundError extends TaggedError("NotFoundError")<{ id: string; message: string; }>() {} class ValidationError extends TaggedError("ValidationError")<{ field: string; message: string; }>() {} type AppError = NotFoundError | ValidationError; // Create errors with object args const err = new NotFoundError({ id: "123", message: "User not found" }); // Exhaustive matching matchError(error, { NotFoundError: (e) => `Missing: ${e.id}`, ValidationError: (e) => `Bad field: ${e.field}`, }); // Partial matching with fallback matchErrorPartial( error, { NotFoundError: (e) => `Missing: ${e.id}` }, (e) => `Unknown: ${e.message}`, ); // Type guards TaggedError.is(value); // any tagged error NotFoundError.is(value); // specific class
For errors with computed messages, add a custom constructor:
class NetworkError extends TaggedError("NetworkError")<{ url: string; status: number; message: string; }>() { constructor(args: { url: string; status: number }) { super({ ...args, message: `Request to ${args.url} failed: ${args.status}` }); } } new NetworkError({ url: "/api", status: 404 });
Serialization
Convert Results to plain objects for RPC, storage, or server actions:
import { Result, SerializedResult } from "better-result"; // Serialize to plain object const result = Result.ok(42); const serialized = Result.serialize(result); // { status: "ok", value: 42 } // Deserialize back to Result instance const deserialized = Result.deserialize<number, never>(serialized); // Ok(42) - can use .map(), .andThen(), etc. // Typed boundary for Next.js server actions async function createUser(data: FormData): Promise<SerializedResult<User, ValidationError>> { const result = await validateAndCreate(data); return Result.serialize(result); } // Client-side const serialized = await createUser(formData); const result = Result.deserialize<User, ValidationError>(serialized);
API Refer
优点
- 优点1
- 优点2
缺点
- 缺点1
- 缺点2
相关技能
免责声明:本内容来源于 GitHub 开源项目,仅供展示和评分分析使用。
版权归原作者所有 dmmulroy.
