Skip to content
Development documentation for the next release. Not a published package edition.
Err examples

Err examples

Error creation, wrapping, codes, metadata, and serialization.

Generated from src/types/err.examples.test.ts . These examples are executable — they run as part of the library’s test suite, so every line below is verified against the released code.

import { describe, expect, test } from "bun:test";
import type { ErrJSON } from "./err";
import { Err } from "./err";

// ══════════════════════════════════════════════════════════════════════════════
// 1. Basic usage
// ══════════════════════════════════════════════════════════════════════════════

describe("Basic usage", () => {
	test("tuple pattern with Err.from and code", () => {
		function divide(a: number, b: number): [number, null] | [null, Err] {
			if (b === 0) {
				return [null, Err.from("Division by zero", "MATH_ERROR")];
			}
			return [a / b, null];
		}

		const [result, err] = divide(10, 0);
		expect(err).not.toBeNull();
		expect(Err.isErr(err)).toBe(true);
		expect(err?.message).toBe("Division by zero");
		expect(err?.code).toBe("MATH_ERROR");
		expect(result).toBeNull();

		const [result2, err2] = divide(10, 2);
		expect(err2).toBeNull();
		expect(result2).toBe(5);
	});

	test("creating error from string with code", () => {
		const err = Err.from("User not found", "NOT_FOUND");
		expect(err.message).toBe("User not found");
		expect(err.code).toBe("NOT_FOUND");
	});

	test("creating error from string with full options", () => {
		const err = Err.from("Connection timeout", {
			code: "TIMEOUT",
			metadata: { host: "api.example.com", timeoutMs: 5000 },
		});
		expect(err.message).toBe("Connection timeout");
		expect(err.code).toBe("TIMEOUT");
		expect(err.metadata).toEqual({
			host: "api.example.com",
			timeoutMs: 5000,
		});
	});

	test("creating error from native Error", () => {
		const nativeErr = new TypeError("Cannot read property");
		const err = Err.from(nativeErr).withCode("OPERATION_FAILED");
		expect(err.message).toBe("Cannot read property");
		expect(err.code).toBe("OPERATION_FAILED");
		expect(err.metadata?.originalName).toBe("TypeError");
	});

	test("creating error from native Error with options", () => {
		try {
			JSON.parse("{invalid");
		} catch (e) {
			const err = Err.from(e as Error, { code: "PARSE_ERROR" });
			expect(err.code).toBe("PARSE_ERROR");
			expect(err.message).toBeTruthy();
		}
	});

	test("creating error from unknown value", () => {
		const err = Err.from(42);
		expect(err.message).toBe("Unknown error");
		expect(err.code).toBe("UNKNOWN");
		expect(err.metadata?.originalValue).toBe(42);
	});

	test("cloning Err with overrides", () => {
		const original = Err.from("Original error");
		const modified = Err.from(original, { code: "NEW_CODE" });
		expect(modified.message).toBe("Original error");
		expect(modified.code).toBe("NEW_CODE");
		expect(original.code).toBeUndefined();
	});

	test("isErr discriminator property", () => {
		const err = Err.from("test");
		expect(err.isErr).toBe(true);
		expect(err.kind).toBe("Err");
	});

	test("Err.isErr type guard", () => {
		const err = Err.from("test");
		expect(Err.isErr(err)).toBe(true);
		expect(Err.isErr("not an error")).toBe(false);
		expect(Err.isErr(null)).toBe(false);
		// Nominal since v0.7.0 — a marker-shaped object is data, not an error.
		expect(Err.isErr({ isErr: true })).toBe(false);
		expect(Err.isErr({ kind: "Err" })).toBe(false);
		// Reconstruct a real instance from wire data instead.
		expect(Err.isErr(Err.from({ kind: "Err", message: "wire" }))).toBe(true);
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 2. Error wrapping
// ══════════════════════════════════════════════════════════════════════════════

describe("Error wrapping", () => {
	test("wrapping with context message", () => {
		const readErr = Err.from("File not found");
		const configErr = readErr.wrap("Failed to read config from /etc/app.json");

		expect(configErr.message).toBe("Failed to read config from /etc/app.json");
		expect(configErr.unwrap()?.message).toBe("File not found");
	});

	test("wrapping with code and metadata via chaining", () => {
		const parseErr = Err.from("Unexpected token");
		const configErr = parseErr
			.wrap("Invalid config format")
			.withCode("CONFIG_ERROR")
			.withMetadata({ path: "/etc/app.json" });

		expect(configErr.message).toBe("Invalid config format");
		expect(configErr.code).toBe("CONFIG_ERROR");
		expect(configErr.metadata?.path).toBe("/etc/app.json");
	});

	test("wrapping with full options object", () => {
		const originalErr = Err.from("Original error");
		const wrapped = originalErr.wrap("Service unavailable", {
			code: "SERVICE_ERROR",
			metadata: { service: "user-service", retryAfter: 30 },
		});

		expect(wrapped.message).toBe("Service unavailable");
		expect(wrapped.code).toBe("SERVICE_ERROR");
		expect(wrapped.metadata).toEqual({
			service: "user-service",
			retryAfter: 30,
		});
		expect(wrapped.unwrap()?.message).toBe("Original error");
	});

	test("accessing root through multiple wraps", () => {
		const root = Err.from("Original error");
		const wrapped = root.wrap("Added context").wrap("More context");

		expect(wrapped.message).toBe("More context");
		expect(wrapped.root.message).toBe("Original error");
	});

	test("chain returns errors from root to current", () => {
		const chain = Err.from("Network timeout")
			.wrap("API request failed")
			.wrap("Could not refresh token")
			.wrap("Authentication failed")
			.chain();

		expect(chain.map((e) => e.message)).toEqual([
			"Network timeout",
			"API request failed",
			"Could not refresh token",
			"Authentication failed",
		]);
	});

	test("unwrap returns immediate cause or undefined", () => {
		const inner = Err.from("DB connection failed");
		const outer = inner.wrap("Could not save user");

		expect(outer.unwrap()?.message).toBe("DB connection failed");
		expect(inner.unwrap()).toBeUndefined();
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 3. Catching native errors
// ══════════════════════════════════════════════════════════════════════════════

describe("Catching native errors", () => {
	test("Err.from + wrap in catch block", () => {
		function parseData(raw: string): [unknown, null] | [null, Err] {
			try {
				return [JSON.parse(raw), null];
			} catch (e) {
				return [null, Err.from(e as Error).wrap("Failed to parse data")];
			}
		}

		const [result, err] = parseData("{invalid}");
		expect(result).toBeNull();
		expect(err).not.toBeNull();
		expect(err?.message).toBe("Failed to parse data");
		expect(err?.unwrap()).not.toBeUndefined();
	});

	test("Err.from + wrap with code and metadata", () => {
		function fetchUser(id: string): [null, Err] {
			const cause = new Error("Connection refused");
			return [
				null,
				Err.from(cause).wrap("Failed to fetch user", {
					code: "USER_FETCH_ERROR",
					metadata: { userId: id },
				}),
			];
		}

		const [, err] = fetchUser("user-42");
		expect(err.message).toBe("Failed to fetch user");
		expect(err.code).toBe("USER_FETCH_ERROR");
		expect(err.metadata?.userId).toBe("user-42");
		expect(err.unwrap()?.message).toBe("Connection refused");
	});

	test("Err.from unknown value in catch block", () => {
		function riskyOp(): [null, Err] {
			try {
				throw "string error";
			} catch (e) {
				return [null, Err.from(e).wrap("Operation failed")];
			}
		}

		const [, err] = riskyOp();
		expect(err.message).toBe("Operation failed");
		expect(err.unwrap()?.message).toBe("string error");
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 4. Error aggregation
// ══════════════════════════════════════════════════════════════════════════════

describe("Error aggregation", () => {
	test("aggregate with add for validation", () => {
		interface UserInput {
			name?: string;
			email?: string;
			age?: number;
		}

		function validateUser(input: UserInput): [UserInput, null] | [null, Err] {
			let errors = Err.from("Validation failed");

			if (!input.name?.trim()) {
				errors = errors.add("Name is required");
			}
			if (!input.email?.includes("@")) {
				errors = errors.add(Err.from("Invalid email", "INVALID_EMAIL"));
			}
			if (input.age !== undefined && input.age < 0) {
				errors = errors.add("Age cannot be negative");
			}

			if (errors.isAggregate) {
				return [null, errors.withCode("VALIDATION_ERROR")];
			}

			return [input, null];
		}

		const [, err] = validateUser({ name: "", email: "bad", age: -1 });
		expect(err).not.toBeNull();
		expect(err?.code).toBe("VALIDATION_ERROR");
	});

	test("aggregate with chained adds", () => {
		const errors = Err.from("Multiple operations failed")
			.add(Err.from("Database write failed"))
			.add(Err.from("Cache invalidation failed"))
			.add("Notification send failed");

		expect(errors.flatten()).toHaveLength(3);
	});

	test("addAll adds multiple errors at once", () => {
		const validationErrors: Array<Err | Error | string> = [
			"Name too short",
			Err.from("Invalid email format").withCode("INVALID_EMAIL"),
			new Error("Age must be positive"),
		];

		const aggregate = Err.from("Validation failed").addAll(validationErrors);
		expect(aggregate.errors.length).toBe(3);
	});

	test("isAggregate distinguishes single from multiple errors", () => {
		const single = Err.from("Single error");
		const multi = Err.from("Multiple").add("One").add("Two");

		expect(single.isAggregate).toBe(false);
		expect(multi.isAggregate).toBe(true);
	});

	test("errors returns direct child errors", () => {
		const aggregate = Err.from("Batch failed")
			.add("Task 1 failed")
			.add("Task 2 failed");

		const messages = aggregate.errors.map((e) => e.message);
		expect(messages).toEqual(["Task 1 failed", "Task 2 failed"]);
	});

	test("flatten recursively collects all leaf errors", () => {
		const nested = Err.from("All errors")
			.add("Error A")
			.add(Err.from("Group B").add("Error B1").add("Error B2"))
			.add("Error C");

		const flat = nested.flatten();
		expect(flat.map((e) => e.message)).toEqual([
			"Error A",
			"Error B1",
			"Error B2",
			"Error C",
		]);
	});

	test("aggregate validation with count check", () => {
		function validate(data: {
			email?: string;
			name?: string;
		}): [typeof data, null] | [null, Err] {
			let errors = Err.from("Validation failed");

			if (!data.email) errors = errors.add("Email is required");
			if (!data.name) errors = errors.add("Name is required");

			if (errors.isAggregate) {
				return [null, errors.withCode("VALIDATION_ERROR")];
			}
			return [data, null];
		}

		const [, err] = validate({});
		expect(err).not.toBeNull();
		expect(err?.code).toBe("VALIDATION_ERROR");

		const [result, err2] = validate({ email: "a@b.com", name: "Test" });
		expect(err2).toBeNull();
		expect(result).toEqual({ email: "a@b.com", name: "Test" });
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 5. JSON serialization
// ══════════════════════════════════════════════════════════════════════════════

describe("JSON serialization", () => {
	test("toJSON produces serializable object", () => {
		const err = Err.from("Not found", {
			code: "NOT_FOUND",
			metadata: { userId: "123" },
		});

		const json = err.toJSON();
		expect(json.message).toBe("Not found");
		expect(json.code).toBe("NOT_FOUND");
		expect(json.metadata).toEqual({ userId: "123" });
		expect(json.timestamp).toBeTruthy();
		expect(json.kind).toBe("Err");
		expect(json.isErr).toBe(true);
	});

	test("toJSON without stack for public APIs", () => {
		const err = Err.from("Not found", "NOT_FOUND");
		const json = err.toJSON({ stack: false });
		expect(json.stack).toBeUndefined();
		expect(json.message).toBe("Not found");
	});

	test("toJSON minimal payload (no stack, no metadata)", () => {
		const err = Err.from("Error", {
			code: "TEST",
			metadata: { secret: "hidden" },
		});
		const json = err.toJSON({ stack: false, metadata: false });
		expect(json.stack).toBeUndefined();
		expect(json.metadata).toBeUndefined();
		expect(json.message).toBe("Error");
		expect(json.code).toBe("TEST");
	});

	test("fromJSON round-trip preserves error structure", () => {
		const original = Err.from("User not found", "NOT_FOUND");
		const json = original.toJSON();
		const restored = Err.fromJSON(json);

		expect(restored.message).toBe("User not found");
		expect(restored.code).toBe("NOT_FOUND");
		expect(Err.isErr(restored)).toBe(true);
	});

	test("fromJSON reconstructs cause chain", () => {
		const original = Err.from("DB error")
			.wrap("Repository failed")
			.wrap("Service error");
		const json = original.toJSON();
		const restored = Err.fromJSON(json);

		expect(restored.message).toBe("Service error");
		expect(restored.unwrap()?.message).toBe("Repository failed");
		expect(restored.root.message).toBe("DB error");
	});

	test("fromJSON reconstructs aggregated errors", () => {
		const original = Err.from("Validation failed")
			.add("Name required")
			.add("Email invalid");
		const json = original.toJSON();
		const restored = Err.fromJSON(json);

		expect(restored.message).toBe("Validation failed");
		expect(restored.errors).toHaveLength(2);
		expect(restored.errors[0]?.message).toBe("Name required");
		expect(restored.errors[1]?.message).toBe("Email invalid");
	});

	test("fromJSON with hasCode check", () => {
		const json: ErrJSON = {
			message: "Not found",
			code: "NOT_FOUND",
			kind: "Err",
			isErr: true,
			timestamp: new Date().toISOString(),
			errors: [],
		};
		const err = Err.fromJSON(json);
		expect(err.hasCode("NOT_FOUND")).toBe(true);
	});

	test("fromJSON throws on invalid input", () => {
		expect(() => Err.fromJSON(null)).toThrow();
		expect(() => Err.fromJSON("string")).toThrow();
		expect(() => Err.fromJSON({})).toThrow(
			"Invalid ErrJSON: message must be a string",
		);
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 6. Metadata
// ══════════════════════════════════════════════════════════════════════════════

describe("Metadata", () => {
	test("withMetadata merges metadata", () => {
		const err = Err.from("Request failed")
			.withMetadata({ url: "/api/users" })
			.withMetadata({ statusCode: 500, retryable: true });

		expect(err.metadata).toEqual({
			url: "/api/users",
			statusCode: 500,
			retryable: true,
		});
	});

	test("withMetadata does not mutate original", () => {
		const original = Err.from("Error").withMetadata({ a: 1 });
		const modified = original.withMetadata({ b: 2 });

		expect(original.metadata).toEqual({ a: 1 });
		expect(modified.metadata).toEqual({ a: 1, b: 2 });
	});

	test("hasMetadata checks value existence by default", () => {
		const err = Err.from("Test").withMetadata({
			url: "/api",
			status: null,
		});

		expect(err.hasMetadata("url")).toBe(true);
		expect(err.hasMetadata("status")).toBe(false);
		expect(err.hasMetadata("status", { keyCheck: true })).toBe(true);
		expect(err.hasMetadata("missing")).toBe(false);
	});

	test("getMetadata retrieves typed values", () => {
		const err = Err.from("Test").withMetadata({
			url: "/api/users",
			count: 42,
			tags: ["important", "retry"],
		});

		expect(err.getMetadata<string>("url")).toBe("/api/users");
		expect(err.getMetadata<number>("count")).toBe(42);
		expect(err.getMetadata("missing")).toBeUndefined();
		expect(err.getMetadata("missing", "default")).toBe("default");
	});

	test("omitMetadata removes single key", () => {
		const err = Err.from("Test", {
			metadata: { url: "/api", token: "secret", retryable: true },
		});

		const sanitized = err.omitMetadata("token");
		expect(sanitized.metadata).toEqual({ url: "/api", retryable: true });
	});

	test("omitMetadata removes multiple keys", () => {
		const err = Err.from("Test", {
			metadata: { url: "/api", token: "secret", retryable: true },
		});

		const minimal = err.omitMetadata(["url", "retryable"]);
		expect(minimal.metadata).toEqual({ token: "secret" });
	});

	test("omitMetadata returns undefined metadata when all keys removed", () => {
		const err = Err.from("Test", {
			metadata: { url: "/api", token: "secret", retryable: true },
		});

		const empty = err.omitMetadata(["url", "token", "retryable"]);
		expect(empty.metadata).toBeUndefined();
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 7. Error codes
// ══════════════════════════════════════════════════════════════════════════════

describe("Error codes", () => {
	test("withCode sets error code", () => {
		const err = Err.from("Record not found").withCode("NOT_FOUND");
		expect(err.code).toBe("NOT_FOUND");
	});

	test("withCode does not mutate original", () => {
		const original = Err.from("Error");
		const withCode = original.withCode("CODE");
		expect(original.code).toBeUndefined();
		expect(withCode.code).toBe("CODE");
	});

	test("hasCode searches cause chain", () => {
		const err = Err.from("DB error", "DB_ERROR")
			.wrap("Repository failed")
			.wrap("Service unavailable");

		expect(err.hasCode("DB_ERROR")).toBe(true);
		expect(err.hasCode("NETWORK_ERROR")).toBe(false);
	});

	test("hasCodePrefix matches hierarchical codes", () => {
		const err = Err.from("Token expired", {
			code: "AUTH:TOKEN:EXPIRED",
		});

		expect(err.hasCodePrefix("AUTH")).toBe(true);
		expect(err.hasCodePrefix("AUTH:TOKEN")).toBe(true);
		expect(err.hasCodePrefix("AUTHORIZATION")).toBe(false);
	});

	test("hasCodePrefix with custom boundary", () => {
		const err = Err.from("Not found", {
			code: "HTTP.404.NOT_FOUND",
		});

		expect(err.hasCodePrefix("HTTP", ".")).toBe(true);
		expect(err.hasCodePrefix("HTTP.404", ".")).toBe(true);
		expect(err.hasCodePrefix("HTTP", ":")).toBe(false);
	});

	test("hasCodePrefix searches error tree", () => {
		const err = Err.from("DB error", { code: "DB:CONNECTION" }).wrap(
			"Service failed",
			{
				code: "SERVICE:UNAVAILABLE",
			},
		);

		expect(err.hasCodePrefix("DB")).toBe(true);
		expect(err.hasCodePrefix("SERVICE")).toBe(true);
	});

	test("find locates first error matching predicate", () => {
		const err = Err.from("Multiple failures")
			.add(Err.from("Not found", "NOT_FOUND"))
			.add(Err.from("Timeout", "TIMEOUT"));

		const timeout = err.find((e) => e.code === "TIMEOUT");
		expect(timeout?.message).toBe("Timeout");
	});

	test("filter finds all errors matching predicate", () => {
		const err = Err.from("Validation failed")
			.add(Err.from("Name required", "REQUIRED"))
			.add(Err.from("Invalid email", "INVALID"))
			.add(Err.from("Age required", "REQUIRED"));

		const required = err.filter((e) => e.code === "REQUIRED");
		expect(required.length).toBe(2);
	});

	test("toString formats error with cause chain", () => {
		const err = Err.from("DB error")
			.wrap("Repository failed")
			.wrap("Service unavailable");

		const str = err.toString();
		expect(str).toContain("[ERROR] Service unavailable");
		expect(str).toContain("Caused by: [ERROR] Repository failed");
		expect(str).toContain("Caused by: [ERROR] DB error");
	});

	test("toString with date and metadata options", () => {
		const err = Err.from("Connection failed", {
			code: "DB:CONNECTION",
			metadata: { host: "localhost", port: 5432 },
		});

		const str = err.toString({ date: true, metadata: true });
		expect(str).toContain(`[${err.timestamp}]`);
		expect(str).toContain("[DB:CONNECTION]");
		expect(str).toContain("Connection failed");
		expect(str).toContain("metadata:");
		expect(str).toContain('"host":"localhost"');
	});

	test("toString formats aggregate errors", () => {
		const err = Err.from("Validation failed", {
			code: "VALIDATION",
		})
			.add("Name required")
			.add("Email invalid");

		const str = err.toString();
		expect(str).toContain("[VALIDATION] Validation failed");
		expect(str).toContain("Errors (2):");
		expect(str).toContain("- [ERROR] Name required");
		expect(str).toContain("- [ERROR] Email invalid");
	});

	test("toString with maxDepth limit", () => {
		const deep = Err.from("Root")
			.wrap("Level 1")
			.wrap("Level 2")
			.wrap("Level 3");

		const str = deep.toString({ maxDepth: 2 });
		expect(str).toContain("[ERROR] Level 3");
		expect(str).toContain("Caused by: [ERROR] Level 2");
		expect(str).toContain("... (1 more cause)");
	});

	test("toError converts to native Error", () => {
		const err = Err.from("Something failed", "MY_ERROR");
		const nativeErr = err.toError();

		expect(nativeErr).toBeInstanceOf(Error);
		expect(nativeErr.message).toBe("Something failed");
		expect(nativeErr.name).toBe("MY_ERROR");
	});

	test("toError preserves cause chain", () => {
		const err = Err.from("Inner").wrap("Outer");
		const nativeErr = err.toError();

		expect(nativeErr.message).toBe("Outer");
		expect(nativeErr.cause).toBeInstanceOf(Error);
		expect((nativeErr.cause as Error).message).toBe("Inner");
	});
});