You do that with a robust, type-safe, and expressive Result-type for TypeScript, inspired by Rust's Result enum. xult provides a simple and powerful way to handle operations that can either succeed (Ok) or fail (Err), without resorting to throwing exceptions.
In many programming languages, errors are handled using exceptions. While exceptions can be useful, they can also make code harder to reason about, especially in asynchronous contexts. xult offers a different approach: representing the result of an operation as a value, which can be either a success (Ok) or a failure (Err).
This makes error handling explicit, predictable, and type-safe.
- ✅ Type-Safe — Leverage TypeScript's type system to ensure you handle both success and error cases.
- ⏳ Async Ready — First-class support for promises and async functions.
- 🌯 Function Wrapping — Easily wrap existing functions to return
Resulttypes. - 👍 Validation — Built-in support for input validation using any library that implements the
@standard-schema/spec. - ❤️ Expressive API — A clean and intuitive API that is a joy to use.
- 🌤️ Lightweight —
xultis a tiny library with zero runtime dependencies. - ✨ Pretty Print — Beautifully outputs
console.logs
The core of xult is the Result<TValue, TError> type, which can be one of two things:
Ok<TValue, never>
Represents a successful result, containing a value of typeTValue.Err<never, TError>
Represents an error, containing an error of typeTErrorwhich extends the error-shape ofResult.ErrorShape = { code: string, message: string, details?: unknown }
xult differs itself from other similar libraries via an intuitive API, types that can be narrowed, simplified and expressed elegantly.
Please see how in Quickstart!
bun add xult
pnpm add xult
npm add xult
When we handle errors, narrowing becomes crucial to providing the best UX/DX feedback:
const result: Result<never, { code: 'INVALID'; details: { issue: unknown } } | { code: 'UNKNOWN' }> if (result.isErr('INVALID')) { // ^? Err<never, { code: 'INVALID'; details: { issue: unknown } }> console.log(result.details.issue) }
Say goodbye to nested
ifstatements and embrace clean, linear logic.xultbrings the power of do-notation to TypeScript through generator functions.import { func, err, ok } from 'xult' const isEven = func((num: number) => { if (num === 0) { return err('ZERO', 'Zero is neither even nor odd.') } return ok(num % 2 === 0) }) // ^? Result<boolean, { code: 'ZERO' }> const processNumber = func(function*(num: number) { // Yield a result. If it's an Err, the function returns it immediately. // If it's an Ok, the value is unwrapped and assigned. const isNumEven = yield* isEven(num) // ^? boolean if (isNumEven) { return `The number ${num} is even.` } return `The number ${num} is odd.` }) // ^? Result<string, { code: 'ZERO' }>When you
yield*aResult,xulthandles the boilerplate: it unwraps the success value or short-circuits the execution with the error. This makes complex, multi-step operations a joy to write.
Depending on your
, you can import xult by:
import Result from 'xult'
Result.ok(...)
Result.err(...)
Result.validate(...)
Result.func(...)
Result.async(...)or (my preference)
import { ok, err, validate, func, async } from 'xult'
import type { Result } from 'xult'You can create Ok and Err results using the static methods on Result:
import { err, ok } from 'xult'
function divide(a: number, b: number) {
if (b === 0) {
return err('DIVISION_BY_ZERO', 'Cannot divide by zero')
}
return ok(a / b)
}Use the isOk() and isErr() methods to check the type of a result. These methods act as type guards, allowing TypeScript to narrow the type of the result.
const result = divide(10, 2)
if (result.isOk()) {
// result is of type Ok<number, never>
console.log(`Result: ${result.value}`) // Output: Result: 5
}
const errorResult = divide(10, 0)
if (errorResult.isErr()) {
// errorResult is of type Err<never, { code: 'DIVISION_BY_ZERO' }>
console.error(`Error: ${errorResult.message}`) // Output: Error: Cannot divide by zero
}You can also check for specific error codes with isErr():
if (errorResult.isErr('DIVISION_BY_ZERO')) {
// This block will run
}Result.async makes it easy to work with promises. It wraps a promise and returns a Promise<Result<...>>. If the promise resolves, it returns an Ok with the resolved value. If the promise rejects, it returns an Err.
import { async, err } from 'xult'
import type { Result } from 'xult'
interface UserDTO {
id: number
name: string
}
async function fetchUser(id: number): Promise<Result<UserDTO, { code: 'FETCH_ERROR' }>> {
const request = fetch(`https://api.example.com/users/${id}`).then(res => res.json() as Promise<UserDTO>)
return async(
request,
thrown => err('FETCH_ERROR', 'Could not retrieve user data.', thrown.details)
)
}
const userResult = await fetchUser(1)
if (userResult.isOk()) {
console.log(userResult.value.name)
} else {
console.error(userResult.message)
}Result.func is a powerful utility for wrapping existing functions to return Result types. It can automatically handle errors, promises, and even input validation.
import { func, err } from 'xult'
const safeParse = func(
JSON.parse,
// optional: handle Result.ThrownError
error => err('INVALID_JSON_STRING', 'Malformatted JSON string', error.details)
)
const result = safeParse('{"name": "John"}') // Result<any, { code: 'INVALID_JSON_STRING', details: unknown }>
if (result.isOk()) {
console.log(result.value.name) // Output: John
}
const errorResult = safeParse('not json')
if (errorResult.isErr()) {
console.error(errorResult.message) // Malformatted JSON string
}Result.func can also validate the arguments of a function using @standard-schema/spec.
import { func } from 'xult'
import v from 'validation-library'
const createUser = func(
[v.string().min(3), v.number().min(18)],
(name: string, age: number) => {
// This code only runs if validation passes
return { name, age }
}
)
const userResult = await createUser('John', 30)
if (userResult.isOk()) {
console.log(userResult.value) // Output: { name: 'John', age: 30 }
}
const validationErrorResult = await createUser('Jo', 17)
if (validationErrorResult.isErr('FUNC_VALIDATION_ERROR')) {
console.error(validationErrorResult.details.issues)
}
@standard-schema/specis a shared interface designed by the authors of Zod, Valibot, and ArkType. Any schema library that implements it can plug intoxult.funcwith zero adapters.
Result.func preserves the this context, allowing you to wrap methods that access instance properties.
import { func, ok } from 'xult'
class User {
constructor(private name: string) {}
// Use a regular function to access `this`
greet = func(function(this: User) {
return ok(`Hello, I am ${this.name}`)
})
}
const user = new User('Alice')
const result = user.greet() // Ok('Hello, I am Alice')This is where xult shines. Generator functions let you write sequential, business-friendly logic without the if (result.isErr()) pyramid.
// Without generators – lots of branching
const processOrder = (input: OrderInput) => {
const validated = validateOrder(input)
if (validated.isErr()) return validated
const payment = chargeCustomer(validated.value)
if (payment.isErr()) return payment
return createOrderRecord(validated.value, payment.value)
}// With generators – linear, expressive, type-safe
import { func } from 'xult'
const processOrder = func(function*(input: OrderInput) {
const payload = yield* validateOrder(input)
const receipt = yield* chargeCustomer(payload)
const order = yield* createOrderRecord(payload, receipt)
return order
})
// ^? Result<Order, ErrorOf<validateOrder> | ErrorOf<chargeCustomer> | ErrorOf<createOrderRecord>>Each yield* unwraps an Ok value or exits early with the originating Err, preserving the original error shape and stack trace.
Result.funcJSON works exactly like Result.func, but automatically converts the result to a JSON-serializable object. This is especially useful for API endpoints where you need to send results over the network. See Serialization.md for more info.
import { funcJSON } from 'xult'
const handler = funcJSON(async function*(req) {
const user = yield* getUser(req.params.id)
const posts = yield* getUserPosts(user.id)
return { user, posts }
})
// Returns: { ok: true, value: { user: {...}, posts: [...] } }
// Or: { ok: false, code: 'NOT_FOUND', message: 'User not found', stack: '...' }The safest strategy is to keep values wrapped until you're ready to handle the error. Either branch explicitly or lean on yield* inside a generator.
import { err, ok } from 'xult'
const divide = (a: number, b: number) => (b === 0 ? err('DIVISION_BY_ZERO', 'Cannot divide by zero') : ok(a / b))
const result = divide(10, 2)
if (result.isErr()) {
return result // short-circuit with the original error
}
// result is Ok here
const value = result.valueWhen you truly know a result is Ok (for example inside tests), _unsafeUnwrap() is available. It rethrows the error with the original metadata if the result is an Err.
import { ok, err } from 'xult'
const okResult = ok(42)
const value = okResult._unsafeUnwrap() // value is 42
const fatal = err('ERROR', 'Something went wrong')
try {
fatal._unsafeUnwrap()
} catch (error) {
if (error instanceof Error) {
console.error(error.message)
}
}Prefer the safe pattern whenever possible—Result makes it ergonomic to avoid exceptions altogether.
xult provides methods to transform values and recover from errors without breaking the chain.
-
result.map(fn)
Transform the value if the result isOk. If it'sErr, the error is passed through.const result = ok(10).map(n => n * 2) // Ok(20)
-
result.catch(fn)
Recover from an error by returning a new value.const result = err('FAIL', 'oops').catch(e => 'recovered') // Ok('recovered')
-
result.ifOk(fn)/result.ifErr(fn)
Execute side-effects (like logging) without changing the result.result.ifErr(e => console.error(e))
xult results are fully serializable, making them easy to send over the network or store.
For more on serialization, see SERIALIZATION.md.
-
result.toJSON()/result.toString()
Convert a result to a plain JSON object or string. -
Result.fromJSON(json)
Restore a result from a JSON string, object, or promise.const result = await Result.fromJSON(fetch('/api/data').then(r => r.json()))
-
Result.tryJSON(json)
Safely attempt to parse a value into a Result. Returnsundefinedif the shape doesn't match. -
Result.isJSON(value)
Check if a value matches the Result JSON shape.
A valid JSON shape contains eitherok: trueorok: false, code: string, message: string, as these are the required properties forResult.okandResult.err -
Result.from(value)
Converts any value into a Result. If the value is already a Result, returns it as-is. If it's a JSON shape ({ ok: true, value }or{ ok: false, code, message }), it's converted to a Result. Otherwise, wraps the value inResult.ok(). Also handles promises.Result.from(123) // Result<number, never> Result.from(Result.ok(42)) // Result<number, never> Result.from({ ok: true, value: 5 }) // Result<number, never> Result.from(Promise.resolve(10)) // Promise<Result<number, never>> Result.from({ ok: false, code: 'ERR', message: 'fail' }) // Result<never, { code: 'ERR' }>
Static helpers
Result.ok(value)– wrap any value in anOkResult.err(code, message, details?)– build structured errors consistentlyResult.async(promise, handleError?)– convert promises intoResultResult.func([schemas?], fn, handleError?)– wrap sync, async, or generator functions (with optional validation)Result.funcJSON([schemas?], fn, handleError?)– same asfunc, but returns JSON-serializable{ ok, value }or{ ok, code, message }shapeResult.validate(schema | schemas, input)– validate inputs using any@standard-schema/specimplementationResult.from(value)– convert any value (plain, Result, JSON shape, or Promise) into a ResultResult.fromJSON(json)– restore a result from its serialised shapeResult.tryJSON(json)– safely parse JSON into Result or undefinedResult.isJSON(value)– check if value is a Result JSON shape
Instance helpers
result.isOk()/result.isErr(code?)– type-guarding checks with optional error-code narrowingresult.map(fn)– transform Ok valueresult.catch(fn)– recover from Errresult.ifOk(fn)/result.ifErr(fn)– side-effectsresult._unsafeUnwrap()– throw ifErr, otherwise return the inner valueresult.toJSON()– serialise a result for transport or storageresult.toString()– JSON string representation
Utilities & types
Result.ValidationError(issues)– standardised validation failure shapeResult.ThrownError(details)– wraps unknown thrown errors- Type exports:
Result.Ok,Result.Err,Result.Any,Result.ValueOf<T>,Result.ErrorOf<T>,Result.ErrorOnly<T>for advanced typing needs
