Skip to main content

Error Patterns

@hex-di/result provides utilities for creating discriminated error types and ensuring exhaustive error handling.

Creating Discriminated Errors

createError(tag, message?, data?)

Creates a discriminated error type with a unique tag for pattern matching.

function createError<TTag extends string, TData = undefined>(
tag: TTag,
message?: string,
data?: TData
): ErrorType<TTag, TData>;

Basic Usage

import { createError, err, type Result } from "@hex-di/result";

// Define error types
const NotFoundError = createError("NotFound");
const ValidationError = createError("Validation");
const NetworkError = createError("Network");

// Create a union type for all app errors
type AppError = typeof NotFoundError | typeof ValidationError | typeof NetworkError;

// Use in functions
function findUser(id: string): Result<User, AppError> {
const user = database.get(id);
if (!user) {
return err(NotFoundError("User not found"));
}
if (!isValidUser(user)) {
return err(ValidationError("Invalid user data"));
}
return ok(user);
}

Errors with Data

Include additional context with your errors:

import { createError } from "@hex-di/result";

// Error with structured data
const ValidationError = createError<"Validation", { fields: string[] }>("Validation");

// Usage
const error = ValidationError("Validation failed", {
fields: ["email", "password"],
});

console.log(error._tag); // 'Validation'
console.log(error.message); // 'Validation failed'
console.log(error.data); // { fields: ['email', 'password'] }

Pattern Matching Errors

Use the discriminated _tag property for exhaustive error handling:

import { createError, err, type Result } from "@hex-di/result";

const NotFoundError = createError("NotFound");
const PermissionError = createError("Permission");
const ValidationError = createError("Validation");

type ApiError = typeof NotFoundError | typeof PermissionError | typeof ValidationError;

function handleApiCall(): Result<Data, ApiError> {
// ... implementation
}

const result = handleApiCall();

if (result.isErr()) {
switch (result.error._tag) {
case "NotFound":
console.log("Resource not found");
return redirectTo404();

case "Permission":
console.log("Access denied");
return redirectToLogin();

case "Validation":
console.log("Invalid input");
return showValidationErrors();

// TypeScript ensures all cases are handled
}
}

Tagged Error Handling

Use catchTag and catchTags to handle specific errors by their _tag, progressively eliminating them from the error union:

import { ok, err, type Result } from "@hex-di/result";

type NotFound = { readonly _tag: "NotFound"; readonly id: string };
type Timeout = { readonly _tag: "Timeout"; readonly ms: number };

const result: Result<string, NotFound | Timeout> = err({ _tag: "NotFound", id: "123" });

// Handle one error type — Timeout remains in the union
const handled = result.catchTag("NotFound", e => ok(`Default for ${e.id}`));

// Handle multiple at once
const allHandled = result.catchTags({
NotFound: e => ok(`Default for ${e.id}`),
Timeout: e => ok(`Retried after ${e.ms}ms`),
});

See the full Tagged Error Handling guide for catchTag, catchTags, andThenWith, and orDie().

createErrorGroup(namespace)

Creates error families with two-level discriminants (_namespace + _tag) for organizing errors across domains:

import { createErrorGroup } from "@hex-di/result";

const Http = createErrorGroup("HttpError");
const NotFound = Http.create("NotFound");
const Timeout = Http.create("Timeout");

// Create error instances with custom fields
const error = NotFound({ url: "/api/users", status: 404 });
// { _namespace: "HttpError", _tag: "NotFound", url: "/api/users", status: 404 }

// Type guards
Http.is(error); // true — belongs to HttpError namespace
Http.isTag("NotFound")(error); // true — is specifically NotFound

Ensuring Exhaustive Handling

assertNever(value)

Ensures exhaustive handling at compile time. TypeScript will error if not all cases are covered.

import { assertNever } from "@hex-di/result";

type AppError =
| { _tag: "NotFound"; message: string }
| { _tag: "Permission"; message: string }
| { _tag: "Validation"; fields: string[] };

function handleError(error: AppError): string {
switch (error._tag) {
case "NotFound":
return `404: ${error.message}`;

case "Permission":
return `403: ${error.message}`;

case "Validation":
return `400: Invalid fields: ${error.fields.join(", ")}`;

default:
// TypeScript error if any case is missing
return assertNever(error);
}
}

If you add a new error type to AppError but forget to handle it in the switch statement, TypeScript will produce a compile-time error at the assertNever call.

Error Pattern Examples

Service Layer Errors

import { createError, ok, err, type Result } from "@hex-di/result";

// Define domain-specific errors
const UserNotFound = createError("UserNotFound");
const EmailTaken = createError("EmailTaken");
const InvalidPassword = createError("InvalidPassword");
const DatabaseError = createError<"DatabaseError", { query: string }>("DatabaseError");

type UserServiceError =
| typeof UserNotFound
| typeof EmailTaken
| typeof InvalidPassword
| typeof DatabaseError;

class UserService {
async createUser(email: string, password: string): Promise<Result<User, UserServiceError>> {
// Check if email exists
const existing = await this.db.findByEmail(email);
if (existing) {
return err(EmailTaken(`Email ${email} is already registered`));
}

// Validate password
if (password.length < 8) {
return err(InvalidPassword("Password must be at least 8 characters"));
}

// Create user
try {
const user = await this.db.create({ email, password });
return ok(user);
} catch (error) {
return err(
DatabaseError("Failed to create user", {
query: "INSERT INTO users...",
})
);
}
}

async authenticate(email: string, password: string): Promise<Result<User, UserServiceError>> {
const user = await this.db.findByEmail(email);
if (!user) {
return err(UserNotFound(`No user with email ${email}`));
}

const valid = await this.checkPassword(user, password);
if (!valid) {
return err(InvalidPassword("Incorrect password"));
}

return ok(user);
}
}

HTTP Error Mapping

import { type Result } from "@hex-di/result";

function errorToHttpStatus(error: AppError): number {
switch (error._tag) {
case "NotFound":
case "UserNotFound":
return 404;

case "Permission":
case "InvalidPassword":
return 403;

case "Validation":
case "EmailTaken":
return 400;

case "DatabaseError":
case "Network":
return 500;

default:
return assertNever(error);
}
}

function sendErrorResponse(res: Response, error: AppError): void {
const status = errorToHttpStatus(error);
const body = {
error: error._tag,
message: error.message,
...(error.data && { details: error.data }),
};

res.status(status).json(body);
}

Composing Error Types

import { type Result } from "@hex-di/result";

// Service-specific errors
type UserError = UserNotFound | InvalidPassword;
type PaymentError = InsufficientFunds | PaymentGatewayError;
type NotificationError = EmailServiceDown | InvalidTemplate;

// Compose for operations that span multiple services
type CheckoutError = UserError | PaymentError | NotificationError;

async function checkout(userId: string, items: CartItem[]): Promise<Result<Order, CheckoutError>> {
// Authenticate user
const user = await userService.getUser(userId);
if (user.isErr()) return user;

// Process payment
const payment = await paymentService.charge(user.value, items);
if (payment.isErr()) return payment;

// Send confirmation
const notification = await notificationService.sendOrderConfirmation(user.value, payment.value);
if (notification.isErr()) return notification;

return ok(createOrder(user.value, payment.value));
}