cloudflare-skill
Comprehensive Cloudflare platform reference docs for AI/LLM consumption. Covers Workers, Pages, storage (KV, D1, R2), AI (Workers AI, Vectorize, Agents SDK), networking, security, and infrastructure-as-code.
Lightweight Result type for TypeScript with generator-based composition.
$ npx -y skills add dmmulroy/better-result --agent claude-code
Repo: dmmulroy/better-result
What's inside
yield* passes successful values to the next step and stops on the first failure.import { Result, TaggedError } from "better-result";
class InvalidPort extends TaggedError("InvalidPort")<{
input: string;
message: string;
}> {}
const parsePort = (input: string) => {
const port = Number(input);
return Number.isInteger(port) && port > 0 && port <= 65_535
? Result.ok(port)
: Result.err(new InvalidPort({ input, message: "Expected a port from 1 to 65535" }));
};
const message = parsePort(process.env.PORT ?? "3000")
.map((port) => `http://localhost:${port}`)
.match({
ok: (address) => `Listening at ${address}`,
err: (error) => `Invalid configuration: ${error.message}`,
});
parsePort returns Result<number, InvalidPort>. Callers cannot use the port until they handle the failure.
npm install better-result
pnpm add better-result
# or: bun add better-result
better-result requires TypeScript 5.4 or newer, is ESM-only, and has zero runtime dependencies.
A Result<T, E> is either a successful Ok<T> or an expected failure Err<E>:
type Result<T, E> = Ok<T, E> | Err<T, E>;
Both variants have a serializable discriminant:
if (userResult.status === "ok") {
renderUser(userResult.value); // User
} else {
reportUserError(userResult.error); // FindUserError
}
Static and instance guards are also available:
if (Result.isOk(userResult)) {
renderUser(userResult.value);
}
if (userResult.isErr()) {
reportUserError(userResult.error);
}
Use Err when the caller can make a meaningful decision about a failure:
Unexpected callback failures and broken invariants are defects. better-result represents those with Panic instead of silently widening a typed error union with unknown.
A useful Result boundary has a caller that can act on the error. Parsers, repositories, adapters, domain operations, and application workflows are good candidates. Pure, total helpers usually are not.
This checkout workflow shows the normal path: define errors, return Results, compose operations, and handle the complete error union.
TaggedError creates real Error subclasses with a literal _tag and typed properties:
import { Result, TaggedError, type Result as ResultType } from "better-result";
class CartNotFound extends TaggedError("CartNotFound")<{
cartId: string;
message: string;
}> {}
class EmptyCart extends TaggedError("EmptyCart")<{
cartId: string;
message: string;
}> {}
class OutOfStock extends TaggedError("OutOfStock")<{
sku: string;
message: string;
}> {}
class PaymentDeclined extends TaggedError("PaymentDeclined")<{
reason: string;
message: string;
}> {}
Tagged errors include normal Error behavior, readonly payload properties, .toJSON(), a class-level .is() guard, exhaustive .match(), and generator support.
const error = new CartNotFound({
cartId: "cart_123",
message: "Cart cart_123 was not found",
});
if (CartNotFound.is(error)) {
console.log(error.cartId);
}
type Cart = {
id: string;
items: ReadonlyArray<{ sku: string; quantity: number }>;
};
const carts = new Map<string, Cart>();
const findCart = (cartId: string): ResultType<Cart, CartNotFound> => {
const cart = carts.get(cartId);
return cart === undefined
? Result.err(new CartNotFound({ cartId, message: "Cart not found" }))
: Result.ok(cart);
};
The error type is part of the function's contract. A caller must propagate, recover from, or handle CartNotFound.
Result.genAssume the application also provides these Result-returning operations:
reserveStock(cart.items); // Result<StockReservation, OutOfStock>
chargePayment(cart, reservation); // Result<Receipt, PaymentDeclined>
Result.gen composes them without nested callbacks or manual early returns:
const checkout = (cartId: string) =>
Result.gen(function* () {
const cart = yield* findCart(cartId);
if (cart.items.length === 0) {
yield* new EmptyCart({ cartId, message: "Cannot check out an empty cart" });
}
const reservation = yield* reserveStock(cart.items);
const receipt = yield* chargePayment(cart, reservation);
return Result.ok(receipt);
});
// Result<Receipt, CartNotFound | EmptyCart | OutOfStock | PaymentDeclined>
Every Ok is unwrapped. The first Err short-circuits the generator. Errors from all yielded Results are collected into the final union.
A tagged error can be yielded directly for a guard clause. This is equivalent to yield* Result.err(new EmptyCart(...)); it returns an Err and does not throw.
Use Result.match to handle success versus failure, then match the tagged error union:
const response = checkout(cartId).match({
ok: (receipt) => Response.json(receipt, { status: 201 }),
err: (error) =>
error.match({
CartNotFound: () => Response.json({ message: "Cart not found" }, { status: 404 }),
EmptyCart: () => Response.json({ message: "Cart is empty" }, { status: 400 }),
OutOfStock: (error) =>
Response.json({ message: `Out of stock: ${error.sku}` }, { status: 409 }),
PaymentDeclined: () => Response.json({ message: "Payment declined" }, { status: 402 }),
}),
});
Adding another tagged error to checkout makes this exhaustive handler fail to type-check until the new policy is defined.
Use matchError when errors are structurally tagged or when data-last composition is more convenient. Use matchErrorPartial when selected variants should be transformed and unhandled variants should pass through.
Prefer Result.gen with Result.await for multi-step asynchronous workflows. It keeps intermediate values local, short-circuits on the first Err, and preserves every yielded error type:
const dashboard = await Result.gen(async function* () {
const session = yield* Result.await(readSession());
const user = yield* Result.await(fetchUser(session.userId));
const posts = yield* Result.await(fetchPosts(user.id));
return Result.ok({ user, posts });
});
// Result<Dashboard, SessionExpired | UserNotFound | FetchPostsFailed>
Result.await provides the async iterator protocol needed by the generator while preserving the Promise's Result types.
For a short pipeline, chain the Promise with static, data-last combinators from the Result namespace:
const postCount = await fetchUser(userId)
.then(Result.andThenAsync((user: User) => fetchPosts(user.id)))
.then(Result.map((posts: ReadonlyArray<Post>) => posts.length));
// Result<number, UserNotFound | FetchPostsFailed>
Promise.then unwraps each outer Promise. Result.andThenAsync runs fetchPosts only for Ok, and Result.map transforms the eventual success while both errors remain visible.
Use this order of preference for asynchronous Result code:
Result.gen with Result.await for workflows with several steps or intermediate values;.then(Result.andThenAsync(...)) and other static combinators for short Promise pipelines;Promise<Result> first only when ordinary control-flow narrowing is clearer than composition.Result.gen closes a short-circuited generator, so finally, Symbol.dispose, and Symbol.asyncDispose cleanup can run. See Generator composition for cleanup and defect behavior.
Use each operation on the branch it owns:
| Operation | Runs on | Purpose |
|---|---|---|
map | Ok | Transform a success value |
mapError | Err | Translate an error value |
andThen | Ok | Continue with another Result-returning operation |
tryRecover | Err | Recover from or replace an error |
tap / tapError | Selected branch | Observe without changing the Result |
match | Both | Leave the Result abstraction with one output |
For example, a profile workflow can keep its errors visible while changing the success value:
const displayName = findUser(userId)
.map((user) => user.profile)
.andThen(validateUserProfile)
.map((profile) => profile.displayName)
.mapError((cause) => new LoadProfileFailed({ cause, message: "Could not load user profile" }));
// Result<string, LoadProfileFailed>
andThen unions errors when the next operation introduces another failure type:
const greeting = findUser(userId).andThen((user) => loadGreeting(user.locale));
// Result<Greeting, UserNotFound | GreetingLoadFailed>
Combinators are available as instance methods and as static data-first or data-last functions:
const upperName = Result.map(userResult, (user) => user.name.toUpperCase());
const getUpperName = Result.map((user: User) => user.name.toUpperCase());
const pipedName = getUpperName(userResult);
See Transforming and chaining for the complete sync and async contracts.
Recovery is different from error transformation: the callback returns another Result and may produce a usable success value.
A cache fallback can recover from a network failure while preserving all other variants:
const user = await fetchUser(userId).then(
Result.tryRecoverAsync(async (error: FetchUserError) =>
error._tag === "NetworkUnavailable" ? await readCachedUser(userId) : Result.err(error),
),
);
// Result<User, UserNotFound | CacheMiss>
Recovery may widen the success type when the fallback returns a different value:
Comprehensive Cloudflare platform reference docs for AI/LLM consumption. Covers Workers, Pages, storage (KV, D1, R2), AI (Workers AI, Vectorize, Agents SDK), networking, security, and infrastructure-as-code.
FAQ
better-result is a Claude Code plugin with 2 hand-picked skills for development work, indexed on Flowy. Install it with the command on its page. It includes adopt-better-result, migrate-better-result-3. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.
Is this plugin yours?
Claim it with GitHubSubmit a pluginPromote it