Result
The Result pattern provides a clean and consistent way to handle success and failure in your code. Instead of relying on exceptions or scattered error handling, Result makes it explicit whether an operation succeeded or failed, improving code readability and reliability.
Key Features
- Inferred user messages - Error messages are automatically derived from
$codevalues - Enhanced debugging - Optional
expectedandgotproperties for detailed error context - Stack traces - Automatic stack capture in development (omitted in production)
- Type safety - Full TypeScript integration with discriminated unions
Create a Result
To create a result type you can use either ok() or err() constructors:
// Success with auto-inferred message
const success = ok(userData, $code.created)
// success.message = "Resource created successfully."
// Error with auto-inferred message
const error = err("Validation failed", $code.badRequest)
// error.message = "Bad request. Please check your input."
// Enhanced error with debugging context
const detailedError = err("Type mismatch", $code.validationTypeError, {
expected: { name: "string", age: "number" },
got: { name: "John", age: "thirty" }
})Act on a Result
To act upon a Result object you can use the $result toolkit:
const res = ok("custom data", $code.ok)
$result(res).isOk // bool
$result(res).isErr // bool
$result(res).map((r) => r.data) // returns this value in ok case.
$result(res).mapErr((e) => String(e)) // transform the error value
$result(res).unwrapOr(1000) // isOk ? `data` : 1000
$result(res).tapOk((data) => console.log(data)) // handle ok case
$result(res).tapErr((err) => console.log(err)) // handle err caseBenefits include:
- Clear separation of success (
Ok) and failure (Err) paths, making your code easier to follow and maintain. - Consistent error handling, allowing you to capture context, messages, and error codes in a structured way.
- Improved observability, as all outcomes are wrapped in a predictable structure, making debugging and logging more straightforward.
- Reduced runtime surprises, since every operation's result is handled explicitly, helping prevent unhandled exceptions.
- TypeScript integration, having strong inference and declarations is a great way to have typescript know whats going on.
- Auto-generated messages, no need to manually write error messages - they're inferred from
$codevalues.
Adopting Result types encourages writing safer, more predictable code by making error cases visible and manageable at every step.
Types
Result
A discriminated union between Ok<T> and Err<E>. Use this when returning data from any async operation or business logic layer.
export type Result<OkResult = unknown, ErrResult = unknown> =
| Expand<Err<ErrResult>>
| Expand<Ok<OkResult>>Ok<T>
Represents a successful result with auto-inferred message.
export type Ok<T, Code extends keyof UserErrorMap = HTTPCode> = {
ok: true
data: T
code: Code
message: UserErrorMap[Code] // ✨ Auto-inferred from $code
stack: StackFrame[] // Dev only
expected: unknown // 🔍 For debugging
got: unknown // 🔍 For debugging
}Err<T>
Represents a failed result with auto-inferred message and debugging context.
export type Err<T, Code extends keyof UserErrorMap = HTTPCode> = {
ok: false
error: T
code: Code
message: UserErrorMap[Code] // ✨ Auto-inferred from $code
stack: StackFrame[] // Dev only
expected: unknown // 🔍 What was expected
got: unknown // 🔍 What was actually received
}The message property is automatically inferred from your $code value using the userErrorMessages mapping. The expected and got properties provide additional debugging context for validation errors.
As we bubble up errors through our system as values we can build a stack trace with meaningful errors.
Expand<T>
Used internally to ensure objects are fully expanded when returned from helpers.
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : neverFactory Functions
ok()
Create an Ok<T> result with auto-inferred message.
ok<Data, Code extends HTTPCode>(
data: Data,
code: Code,
options?: { expected?: unknown; got?: unknown }
)Parameters:
data: your success payloadcode: HTTP status code from$code(message auto-inferred)options: optional debugging context
Examples:
// Basic success
ok({ userId: "abc123" }, $code.ok)
// Result: { ok: true, data: {...}, message: "Request succeeded." }
// Success with debugging context
ok(userData, $code.created, {
expected: "User object with email",
got: userData
})err()
Create an Err<T> result with auto-inferred message and debugging context.
err<Data, Code extends HTTPCode>(
data: Data,
code: Code,
options?: { expected?: unknown; got?: unknown }
)Parameters:
data: your error context (string, object, etc)code: HTTP error code from$code(message auto-inferred)options: debugging context - what was expected vs what was received
Examples:
// Basic error
err("Missing token", $code.unauthorized)
// Result: { ok: false, error: "Missing token", message: "Unauthorized. Please log in to continue." }
// Validation error with context
err("Type validation failed", $code.validationTypeError, {
expected: { name: "string", age: "number" },
got: { name: "John", age: "thirty" }
})
// Result includes debugging info for easier troubleshooting
// Custom application error
err("User not found", $code.emailDoesntExist)
// Result: { ..., message: "Sorry, we can't find your email. Please create an account first." }✨ Message Inference: All error messages are automatically derived from your $code value using the userErrorMessages mapping - no need to write custom error messages!
🔍 Enhanced Debugging: Use expected and got properties to provide detailed context about validation failures, making debugging much easier.
$result Utility
Helper for working with a Result<T, E> value. Wrap your result to gain access to helper methods for safe unwrapping, transforming, or matching behavior.
const $res = $result(myResult).isOk / .isErr
Boolean flags.
if ($res.isOk) doSomething($res.unwrap()).map(fn)
Transform the success value.
$res.map((user) => user.name).mapErr(fn)
Transform the error value.
$res.mapErr((err) => `Oops: ${err}`).flatMap(fn)
Chain another result-producing function if ok.
$res.flatMap(fetchUserDetails)tapErr()
Run a function if the result is an err.
$res.tapErr((e) => console.log(e))tapOk()
Run a function if the result is ok.
$res.tapOk((o) => console.log(o)).unwrap()
Returns the success value or throws if it’s an error.
const user = $res.unwrap() // throws if .isErr.unwrapOr(fallback)
Returns the success value or a fallback if error.
const user = $res.unwrapOr({ name: "Guest" }).match({ ok, err })
Pattern match for clean logic split.
const greeting = $res.match({
ok: ({ data }) => `Hi ${data.name}`,
err: ({ error }) => `Error: ${String(error)}`,
})Usage Examples
Basic Usage
async function fetchUser(id: string): Promise<Result<User, string>> {
try {
const data = await api.get(`/users/${id}`)
return ok(data, $code.ok) // message: "Request succeeded."
} catch (e) {
return err("Failed to fetch user", $code.unknown) // message: "An unknown error occurred. Please try again later."
}
}
const userResult = await fetchUser("abc")
const $res = $result(userResult)
$res.match({
ok: ({ data, message }) => console.log(message, "Welcome", data.name),
err: ({ error, message }) => console.error(message, error),
})Enhanced Debugging with Expected/Got
function validateUser(input: any): Result<User, string> {
const expectedSchema = {
name: "string",
email: "string.email",
age: "number"
}
if (typeof input.name !== 'string') {
return err("Invalid name type", $code.validationTypeError, {
expected: expectedSchema,
got: input
})
}
if (!input.email?.includes('@')) {
return err("Invalid email format", $code.invalidEmail, {
expected: "Valid email with @ symbol",
got: input.email
})
}
return ok(input as User, $code.ok)
}
// Usage
const result = validateUser({ name: 123, email: "invalid", age: 25 })
$result(result).match({
ok: ({ data }) => console.log("Valid user:", data),
err: ({ error, message, expected, got }) => {
console.error("Validation failed:", message)
console.error("Error:", error)
console.error("Expected:", expected)
console.error("Got:", got)
// This provides rich debugging context!
}
})API Response with Auto-Generated Messages
async function createUser(userData: any): Promise<Result<User, string>> {
// Validation
if (!userData.email) {
return err("Email is required", $code.validationRequiredError)
// Auto message: "Required field is missing."
}
if (await userExists(userData.email)) {
return err("User already exists", $code.emailAlreadyExists)
// Auto message: "This email is already registered with us. Please login instead."
}
try {
const user = await saveUser(userData)
return ok(user, $code.created)
// Auto message: "Resource created successfully."
} catch (error) {
return err("Database error", $code.unknown, {
expected: "User saved successfully",
got: error
})
}
}
// The client gets user-friendly messages automatically
const result = await createUser(formData)
$result(result).match({
ok: ({ data, message }) => {
showSuccessToast(message) // "Resource created successfully."
navigateTo('/dashboard')
},
err: ({ message, error }) => {
showErrorToast(message) // User-friendly message from $code
console.error("Details:", error) // Technical details for debugging
}
})
## When to Use
- ✅ You want better-than-boolean success handling
- ✅ You want to avoid try/catch soup
- ✅ You want stack traces + status codes for debugging in prod
## Related Concepts
- Similar to `Result`/`Option` in Rust or `Either` in Haskell
- Can be extended to a full monad-style error system
- Easy to pair with `Option` types for nullable return values
This system enforces clarity in data handling: no more guessing if `null` is a valid value or an error. Either it worked, or it didn’t — and you know exactly why.
Go forth and handle failure _with dignity._