Result Type
Handlers return a Result<A, E> type for explicit error handling, powered by @alt-stack/result.
For comprehensive documentation on the Result type, see the Result documentation.
Overview
The Result type makes errors explicit in your type signatures. Instead of throwing exceptions, handlers return Result types that are either Ok (success) or Err (failure).
import { ok, err, TaggedError } from "@alt-stack/server-hono";
class NotFoundError extends TaggedError {
readonly _tag = "NotFoundError";
constructor(public readonly resourceId: string) {
super(`Resource ${resourceId} not found`);
}
}
// Success: wrap value in ok()
return ok({ id: "123", name: "John" });
// Error: wrap error in err()
return err(new NotFoundError("123"));
Defining Error Classes
Define your error classes using TaggedError:
import { TaggedError } from "@alt-stack/result";
class NotFoundError extends TaggedError {
readonly _tag = "NotFoundError";
constructor(public readonly resourceId: string) {
super(`Resource ${resourceId} not found`);
}
}
class ForbiddenError extends TaggedError {
readonly _tag = "ForbiddenError";
constructor(public readonly message: string = "Access denied") {
super(message);
}
}
Basic Usage
Returning Success
Use ok() to return successful values:
const handler = procedure
.output(z.object({ id: z.string(), name: z.string() }))
.get(({ input }) => {
const user = { id: "123", name: "John" };
return ok(user);
});
Returning Errors
Use err() to return typed errors:
class NotFoundError extends TaggedError {
readonly _tag = "NotFoundError";
constructor(public readonly resourceId: string) {
super(`Resource ${resourceId} not found`);
}
}
const handler = procedure
.output(UserSchema)
.errors({
404: z.object({
_tag: z.literal("NotFoundError"),
resourceId: z.string(),
}),
})
.get(({ input }) => {
const user = findUser(input.params.id);
if (!user) {
return err(new NotFoundError(input.params.id));
}
return ok(user);
});
Error Schema Requirements
Error schemas must include a _tag field with a z.literal() value:
// ✅ Valid - has _tag literal
.errors({
404: z.object({
_tag: z.literal("NotFoundError"),
resourceId: z.string(),
}),
})
// ❌ Invalid - missing _tag (compile error)
.errors({
404: z.object({
message: z.string(),
}),
})
// ❌ Invalid - _tag is string, not literal (compile error)
.errors({
404: z.object({
_tag: z.string(),
message: z.string(),
}),
})
Type Inference
Error types are inferred from your .errors() definitions. TypeScript ensures you can only return errors that match declared schemas:
class NotFoundError extends TaggedError {
readonly _tag = "NotFoundError";
constructor(public readonly resourceId: string) {
super(`Not found: ${resourceId}`);
}
}
class ConflictError extends TaggedError {
readonly _tag = "ConflictError";
constructor(public readonly message: string) {
super(message);
}
}
procedure
.errors({
404: z.object({ _tag: z.literal("NotFoundError"), resourceId: z.string() }),
409: z.object({ _tag: z.literal("ConflictError"), message: z.string() }),
})
.get(({ input }) => {
// TypeScript knows errors must match 404 or 409 schemas
if (!exists) {
return err(new NotFoundError(input.params.id)); // ✅ Compiles
}
if (conflict) {
return err(new ConflictError("Already exists")); // ✅ Compiles
}
return ok(result);
});
Result Utilities
The server packages re-export Result utilities from @alt-stack/result:
Type Guards
import { isOk, isErr } from "@alt-stack/server-hono";
const result = await handler();
if (isOk(result)) {
console.log(result.value);
}
if (isErr(result)) {
console.log(result.error);
}
Pattern Matching
import { match } from "@alt-stack/server-hono";
const message = match(result, {
ok: (value) => `Success: ${value.name}`,
err: (error) => `Error: ${error.message}`,
});
Transformations
import { map, flatMap, mapError } from "@alt-stack/server-hono";
// Transform success value
const mapped = map(result, (user) => user.name);
// Chain operations
const chained = flatMap(result, (user) => {
if (!user.active) return err(new ForbiddenError("Inactive user"));
return ok(user.profile);
});
// Transform error
const withNewError = mapError(result, (e) => ({ ...e, logged: true }));
Extraction
import { unwrap, unwrapOr } from "@alt-stack/server-hono";
// Get value or throw (use sparingly)
const value = unwrap(result);
// Get value or default
const valueOrDefault = unwrapOr(result, defaultUser);
Try-Catch Wrappers
import { tryCatch, tryCatchAsync } from "@alt-stack/server-hono";
class ParseError extends TaggedError {
readonly _tag = "ParseError";
constructor(public readonly message: string) {
super(message);
}
}
// Wrap sync function
const result = tryCatch(
() => JSON.parse(input),
(e) => new ParseError("Invalid JSON")
);
// Wrap async function
const asyncResult = await tryCatchAsync(
() => fetchUser(id),
(e) => new FetchError(String(e))
);
Middleware
Middleware can return err() just like handlers. Define errors with .errors() before .use():
class UnauthorizedError extends TaggedError {
readonly _tag = "UnauthorizedError";
constructor(public readonly message: string = "Authentication required") {
super(message);
}
}
const protectedProcedure = procedure
.errors({
401: z.object({
_tag: z.literal("UnauthorizedError"),
message: z.string(),
}),
})
.use(async ({ ctx, next }) => {
if (!ctx.user) {
return err(new UnauthorizedError());
}
return next({ ctx: { user: ctx.user } });
});
See Also
- Result Documentation - Complete guide to the Result type
- Error Handling - Defining error schemas