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

Outcome examples

Mapping, piping, combinators, and error handling (deprecated tier).

Generated from src/types/outcome.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 { Err } from "./err";
import type { ResultTuple } from "./outcome";
import { Outcome } from "./outcome";

// ══════════════════════════════════════════════════════════════════════════════
// 1. Creating outcomes
// ══════════════════════════════════════════════════════════════════════════════

describe("Creating outcomes", () => {
	test("Outcome.ok() creates a success outcome", () => {
		const [val, err] = Outcome.ok(42).toTuple();
		expect(val).toBe(42);
		expect(err).toBeNull();
	});

	test("Outcome.err() creates an error outcome from Err instance", () => {
		const err = Err.from("Something failed");
		const [value, error] = Outcome.err(err).toTuple();
		expect(value).toBeNull();
		expect(error?.message).toBe("Something failed");
	});

	test("Outcome.err() creates an error outcome from message with code", () => {
		const [, err] = Outcome.err("Not found", "NOT_FOUND").toTuple();
		expect(err?.code).toBe("NOT_FOUND");
	});

	test("Outcome.err() creates an error outcome from message with options", () => {
		const [, err] = Outcome.err("Timeout", {
			code: "TIMEOUT",
			metadata: { durationMs: 5000 },
		}).toTuple();
		expect(err?.code).toBe("TIMEOUT");
		expect(err?.metadata).toEqual({ durationMs: 5000 });
	});

	test("Outcome.err() wraps a native Error", () => {
		const nativeErr = new Error("parse error");
		const [, err] = Outcome.err("Parse failed", nativeErr, {
			code: "PARSE_ERROR",
		}).toTuple();
		expect(err?.message).toBe("Parse failed");
		expect(err?.code).toBe("PARSE_ERROR");
		expect(err?.unwrap()).not.toBeUndefined();
	});

	test("Outcome.err<T>() claims a success type so the chain keeps working", () => {
		const count = Outcome.err<number>("bad input").defaultTo(0);
		expect(count).toBe(0);
	});

	test("Outcome.ok() creates a void success", () => {
		const [val, err] = Outcome.ok().toTuple();
		expect(val).toBeUndefined();
		expect(err).toBeNull();
	});

	test("Outcome.ok(null) carries an explicit null value", () => {
		const [val, err] = Outcome.ok(null).toTuple();
		expect(val).toBeNull();
		expect(err).toBeNull();
	});

	test("toTuple() is the single extraction point, narrowing both branches", () => {
		const describeOutcome = (outcome: Outcome<number>): string => {
			const [val, err] = outcome.toTuple();
			if (err !== null) return `failed: ${err.message}`;
			return `ok: ${val.toFixed(1)}`;
		};

		expect(describeOutcome(Outcome.ok(42))).toBe("ok: 42.0");
		expect(describeOutcome(Outcome.err<number>("Failed"))).toBe(
			"failed: Failed",
		);
	});

	test("collections narrow once mapped to tuples", () => {
		const list: Outcome<number>[] = [
			Outcome.ok(1),
			Outcome.err<number>("skipped"),
			Outcome.ok(3),
		];

		const values: number[] = list
			.map((o) => o.toTuple())
			.filter((t) => t[1] === null)
			.map((t) => t[0]);

		expect(values).toEqual([1, 3]);
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 2. Factory methods
// ══════════════════════════════════════════════════════════════════════════════

describe("Factory methods", () => {
	test("Outcome.from() carries whatever the callback returns", () => {
		const [val, err] = Outcome.from(() => 42).toTuple();
		expect(val).toBe(42);
		expect(err).toBeNull();
	});

	test("Outcome.from() with Err shorthand", () => {
		const [, err] = Outcome.from(() => Err.from("Invalid input")).toTuple();
		expect(err?.message).toBe("Invalid input");
	});

	test("Outcome.from() passes a returned Outcome through", () => {
		const [val] = Outcome.from(() => Outcome.ok("inner")).toTuple();
		expect(val).toBe("inner");
	});

	test("Outcome.from() catches throws from external libraries", () => {
		const [, err] = Outcome.from(() => JSON.parse("not valid json")).toTuple();
		expect(err).not.toBeNull();
	});

	test("Outcome.ok() is how an Err becomes a success value", () => {
		const carried = Err.from("validation detail");

		const asFailure = Outcome.from(() => carried).toTuple();
		const asValue = Outcome.from(() => Outcome.ok(carried)).toTuple();

		expect(asFailure[1]).toBe(carried);
		expect(asValue[0]).toBe(carried);
	});

	test("Outcome.fromAsync() with success", async () => {
		const [val, err] = (await Outcome.fromAsync(async () => 42)).toTuple();
		expect(val).toBe(42);
		expect(err).toBeNull();
	});

	test("Outcome.fromAsync() with error", async () => {
		const [, err] = (
			await Outcome.fromAsync(async () =>
				Err.from("Request failed", { code: "HTTP_ERROR" }),
			)
		).toTuple();
		expect(err?.code).toBe("HTTP_ERROR");
	});

	test("Outcome.fromAsync() catches async throws", async () => {
		const [, err] = (
			await Outcome.fromAsync(async () => {
				throw new Error("network failure");
			})
		).toTuple();
		expect(err).not.toBeNull();
	});

	test("Outcome.fromAsync() with error aggregation pattern", async () => {
		const [, err] = (
			await Outcome.fromAsync(async () => {
				let errors = Err.from("Batch failed");
				errors = errors.add(Err.from("task A failed"));
				errors = errors.add(Err.from("task B failed"));

				if (errors.isAggregate) return errors;
				return null;
			})
		).toTuple();
		expect(err?.isAggregate).toBe(true);
	});

	test("Outcome.fromTuple() from a tuple", () => {
		const tuple: ResultTuple<string> = ["hello", null];
		expect(Outcome.fromTuple(tuple).toTuple()[0]).toBe("hello");
	});

	test("Outcome.fromTuple() wraps a Go-style function", () => {
		// The tuple protocol lives here and nowhere else.
		const parsePort = (raw: string) =>
			Outcome.fromTuple(() => {
				const n = Number(raw);
				if (Number.isNaN(n)) return [null, Err.from("not a number", "PARSE")];
				return [n, null];
			});

		expect(parsePort("8080").toTuple()[0]).toBe(8080);
		expect(parsePort("nope").toTuple()[1]?.code).toBe("PARSE");
	});

	test("Outcome.fromTupleAsync() wraps an async Go-style function", async () => {
		const load = async (id: string): Promise<ResultTuple<{ id: string }>> =>
			id === "1" ? [{ id }, null] : [null, Err.from("missing", "NOT_FOUND")];

		const found = await Outcome.fromTupleAsync(() => load("1"));
		const missing = await Outcome.fromTupleAsync(() => load("2"));

		expect(found.toTuple()[0]).toEqual({ id: "1" });
		expect(missing.toTuple()[1]?.code).toBe("NOT_FOUND");
	});

	test("Outcome.fromTuple() round-trip", () => {
		const tuple = Outcome.ok(42).toTuple();
		expect(Outcome.fromTuple(tuple).toTuple()[0]).toBe(42);
	});

	test("a tuple returned from Outcome.from() is a plain value", () => {
		// Under the value protocol only Err and Outcome are control flow.
		const [val, err] = Outcome.fromTuple<[number, null]>(() => [
			[1, null],
			null,
		]).toTuple();
		expect(val).toEqual([1, null]);
		expect(err).toBeNull();
	});

	test("Outcome.fromJSON() round-trip", () => {
		const json = JSON.stringify(Outcome.ok({ name: "John" }).toJSON());
		const restored = Outcome.fromJSON(JSON.parse(json));
		expect(restored.toTuple()[0]).toEqual({ name: "John" });
	});

	test("Outcome.fromJSON() with invalid payload", () => {
		const [, err] = Outcome.fromJSON({ not: "a tuple" } as unknown as [
			unknown,
			null,
		]).toTuple();
		expect(err?.message).toBe("Invalid Outcome JSON");
	});

	test("Outcome.fromJSON() with error outcome round-trip", () => {
		const json = JSON.stringify(
			Outcome.err("Something failed", "FAIL_CODE").toJSON(),
		);
		const [, err] = Outcome.fromJSON(JSON.parse(json)).toTuple();
		expect(err?.message).toBe("Something failed");
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 3. Transformations
// ══════════════════════════════════════════════════════════════════════════════

describe("Transformations", () => {
	test("map() simple transformation", () => {
		const [val] = Outcome.ok(5)
			.map((n) => n * 2)
			.map((n) => n.toString())
			.toTuple();
		expect(val).toBe("10");
	});

	test("map() carries the return value as-is, never as control flow", () => {
		const [val, err] = Outcome.ok(5)
			.map(() => Err.from("this is data"))
			.toTuple();
		expect(val).toBeInstanceOf(Err);
		expect(err).toBeNull();
	});

	test("flatMap() transformation that can fail", () => {
		const [val, err] = Outcome.ok('{"name":"John"}')
			.flatMap((json) => Outcome.from(() => JSON.parse(json)))
			.toTuple();
		expect(err).toBeNull();
		expect(val).toEqual({ name: "John" });
	});

	test("flatMap() transformation that fails with invalid JSON", () => {
		const [, err] = Outcome.ok("not json")
			.flatMap((json) =>
				Outcome.from(() => JSON.parse(json)).mapErr((e) =>
					e.wrap("Invalid JSON"),
				),
			)
			.toTuple();
		expect(err?.message).toBe("Invalid JSON");
	});

	test("flatMap() treats a bare Err return as the failure", () => {
		const [, err] = Outcome.ok(10)
			.flatMap((n) => (n > 5 ? Err.from("Too big", "RANGE") : n))
			.toTuple();
		expect(err?.code).toBe("RANGE");
	});

	test("flatMap() short-circuits on the returned error", () => {
		const [, err] = Outcome.ok(10)
			.flatMap((n) => (n > 5 ? Outcome.err<number>("Too big", "RANGE") : n))
			.map((n) => n * 2)
			.toTuple();
		expect(err?.code).toBe("RANGE");
	});

	test("map() error passes through", () => {
		const [, err] = Outcome.err<number>("Original error")
			.map((v) => v)
			.toTuple();
		expect(err?.message).toBe("Original error");
	});

	test("mapAsync() transforms success value", async () => {
		const outcome = await Outcome.ok("user-123").mapAsync(async (id) => ({
			id,
			name: "John",
		}));
		expect(outcome.toTuple()[0]).toEqual({ id: "user-123", name: "John" });
	});

	test("flatMapAsync() chains an async step that can fail", async () => {
		const load = async (id: string) =>
			id === "user-123"
				? Outcome.ok({ id, name: "John" })
				: Outcome.err<{ id: string; name: string }>("Not found", "NOT_FOUND");

		const found = await Outcome.ok("user-123").flatMapAsync(load);
		const missing = await Outcome.ok("nobody").flatMapAsync(load);

		expect(found.toTuple()[0]).toEqual({ id: "user-123", name: "John" });
		expect(missing.toTuple()[1]?.code).toBe("NOT_FOUND");
	});

	test("mapErr() recovery from error", () => {
		const [val, err] = Outcome.err<string>("Not found", "NOT_FOUND")
			.mapErr((e) => (e.hasCode("NOT_FOUND") ? "default" : e))
			.toTuple();
		expect(err).toBeNull();
		expect(val).toBe("default");
	});

	test("mapErr() error transformation via wrap", () => {
		const [, err] = Outcome.err("Low-level error")
			.mapErr((e) => e.wrap("High-level context"))
			.toTuple();
		expect(err?.message).toBe("High-level context");
		expect(err?.unwrap()).not.toBeUndefined();
	});

	test("mapErr() recovery with a returned Outcome", () => {
		const [val, err] = Outcome.err("Primary failed")
			.mapErr(() => Outcome.ok("secondary"))
			.toTuple();
		expect(err).toBeNull();
		expect(val).toBe("secondary");
	});

	test("mapErr() passes through on success", () => {
		const [val] = Outcome.ok(42)
			.mapErr((e) => e)
			.toTuple();
		expect(val).toBe(42);
	});

	test("mapErrAsync() async recovery", async () => {
		const outcome = await Outcome.err<string>("Primary failed").mapErrAsync(
			async (e) => {
				const fallback = "backup-data";
				if (fallback) return fallback;
				return e.wrap("Backup also failed");
			},
		);
		expect(outcome.toTuple()[0]).toBe("backup-data");
	});

	test("pipe() basic pipeline", () => {
		const validate = (s: string) => (s.length > 0 ? s : Err.from("empty"));

		const [val] = Outcome.ok("hello")
			.pipe(
				([str, err]) => (err ? err : validate(str)),
				([str, err]) => (err ? err : str.toUpperCase()),
			)
			.toTuple();
		expect(val).toBe("HELLO");
	});

	test("pipe() mid-chain recovery", () => {
		const DEFAULT_VALUE = "recovered";

		const [val, err] = Outcome.ok("input")
			.pipe(
				([, e]) => (e ? e : Err.from("Invalid", "VALIDATION")),
				([str, e]) => {
					if (e?.hasCode("VALIDATION")) return DEFAULT_VALUE;
					if (e) return e;
					return str;
				},
			)
			.toTuple();
		expect(err).toBeNull();
		expect(val).toBe("recovered");
	});

	test("pipeAsync() async pipeline", async () => {
		const result = await Outcome.ok("user-1").pipeAsync(
			async ([id, err]) => (err ? err : { id, name: "Alice" }),
			async ([user, err]) => (err ? err : { ...user, bio: "Developer" }),
		);
		expect(result.toTuple()[0]).toEqual({
			id: "user-1",
			name: "Alice",
			bio: "Developer",
		});
	});

	test("pipeAsync() async recovery", async () => {
		const result = await Outcome.ok("id-1").pipeAsync(
			async ([, err]) => (err ? err : Err.from("primary down")),
			async ([val, err]) => (err ? "fallback-data" : val),
		);
		expect(result.toTuple()[0]).toBe("fallback-data");
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 4. Terminal operations
// ══════════════════════════════════════════════════════════════════════════════

describe("Terminal operations", () => {
	test("toTuple() extracts value and null error on success", () => {
		const [value, error] = Outcome.ok(42).toTuple();
		expect(value).toBe(42);
		expect(error).toBeNull();
	});

	test("toTuple() extracts null value and error on failure", () => {
		const [value, error] = Outcome.err("Failed").toTuple();
		expect(value).toBeNull();
		expect(error?.message).toBe("Failed");
	});

	test("defaultTo() with static fallback", () => {
		expect(Outcome.err<number>("bad input").defaultTo(0)).toBe(0);
	});

	test("defaultTo() returns success value when ok", () => {
		expect(Outcome.ok(42).defaultTo(0)).toBe(42);
	});

	test("defaultTo() with object fallback", () => {
		const config = Outcome.err<{ port: number; host: string }>(
			"no config",
		).defaultTo({ port: 3000, host: "localhost" });
		expect(config).toEqual({ port: 3000, host: "localhost" });
	});

	test("defaultTo() with computed fallback from error", () => {
		const name = Outcome.err<string>("Not found", "NOT_FOUND").defaultTo(
			(err) => (err.hasCode("NOT_FOUND") ? "Guest" : "Unknown"),
		);
		expect(name).toBe("Guest");
	});

	test("defaultTo() with function as value using asValue flag", () => {
		const defaultHandler = () => "default";
		const handler = Outcome.err<() => string>("no handler").defaultTo(
			defaultHandler,
			true,
		);
		expect(handler).toBe(defaultHandler);
		expect(handler()).toBe("default");
	});

	test("either() basic transformation", () => {
		const successMessage = Outcome.ok({ name: "John" }).either(
			(user) => `Welcome, ${user.name}!`,
			(err) => `Error: ${err.message}`,
		);
		expect(successMessage).toBe("Welcome, John!");

		const errorMessage = Outcome.err<{ name: string }>(
			"Connection lost",
		).either(
			() => "ok",
			(err) => `Error: ${err.message}`,
		);
		expect(errorMessage).toBe("Error: Connection lost");
	});

	test("either() default value on error", () => {
		const count = Outcome.err<number>("bad").either(
			(n) => n,
			() => 0,
		);
		expect(count).toBe(0);
	});

	test("either() type transformation", () => {
		const successStatus: "success" | "error" = Outcome.ok(42).either(
			() => "success" as const,
			() => "error" as const,
		);
		expect(successStatus).toBe("success");

		const errorStatus: "success" | "error" = Outcome.err<number>("fail").either(
			() => "success" as const,
			() => "error" as const,
		);
		expect(errorStatus).toBe("error");
	});

	test("either() HTTP response building pattern", () => {
		type HttpResponse = { status: number; body: Record<string, unknown> };

		const successResponse = Outcome.ok({
			id: "ord-1",
			total: 99.99,
		}).either<HttpResponse>(
			(order) => ({
				status: 200,
				body: { id: order.id, total: order.total },
			}),
			(err) => ({
				status: 500,
				body: { error: err.message },
			}),
		);
		expect(successResponse.status).toBe(200);
		expect(successResponse.body).toEqual({ id: "ord-1", total: 99.99 });

		const errorResponse = Outcome.err<{ id: string }>(
			"Not found",
			"NOT_FOUND",
		).either<HttpResponse>(
			() => ({ status: 200, body: { id: "x" } }),
			(err) => ({
				status: err.hasCode("NOT_FOUND") ? 404 : 500,
				body: { error: err.message },
			}),
		);
		expect(errorResponse.status).toBe(404);
		expect(errorResponse.body).toEqual({ error: "Not found" });
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 5. Side effects
// ══════════════════════════════════════════════════════════════════════════════

describe("Side effects", () => {
	test("effect() runs side effect on success and chains", () => {
		let logged = "";

		const [val] = Outcome.ok(42)
			.effect(([value, err]) => {
				logged = err ? `Failed: ${err.message}` : `Success: ${value}`;
			})
			.map((v) => v * 2)
			.toTuple();

		expect(logged).toBe("Success: 42");
		expect(val).toBe(84);
	});

	test("effect() runs side effect on error", () => {
		let logged = "";

		Outcome.err("Something broke").effect(([value, err]) => {
			logged = err ? `Failed: ${err.message}` : `Success: ${value}`;
		});

		expect(logged).toBe("Failed: Something broke");
	});

	test("effect() records metrics-like data", () => {
		const metrics: {
			success: boolean;
			value: unknown;
			errorCode?: string;
		}[] = [];

		Outcome.ok(42).effect(([val, err]) => {
			metrics.push({
				success: !err,
				value: val,
				errorCode: err?.code,
			});
		});

		expect(metrics).toHaveLength(1);
		expect(metrics[0]).toEqual({
			success: true,
			value: 42,
			errorCode: undefined,
		});
	});

	test("effect() becomes error outcome if callback throws", () => {
		const [, err] = Outcome.ok(42)
			.effect(() => {
				throw new Error("effect blew up");
			})
			.toTuple();
		expect(err?.message).toBe("effect blew up");
	});

	test("effectAsync() runs async side effect", async () => {
		let logged = false;

		const outcome = await Outcome.ok({ data: "test" }).effectAsync(async () => {
			logged = true;
		});

		expect(logged).toBe(true);
		expect(outcome.toTuple()[0]).toEqual({ data: "test" });
	});

	test("effectAsync() becomes error if async callback throws", async () => {
		const outcome = await Outcome.ok(1).effectAsync(async () => {
			throw new Error("async effect failed");
		});
		expect(outcome.toTuple()[1]).not.toBeNull();
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 6. Combinators
// ══════════════════════════════════════════════════════════════════════════════

describe("Combinators", () => {
	test("Outcome.all() succeeds when all succeed", () => {
		const combined = Outcome.all([Outcome.ok(1), Outcome.ok(2), Outcome.ok(3)]);
		expect(combined.toTuple()[0]).toEqual([1, 2, 3]);
	});

	test("Outcome.all() fails when one fails", () => {
		const [, err] = Outcome.all([
			Outcome.ok(1),
			Outcome.err<number>("Failed"),
			Outcome.ok(3),
		]).toTuple();
		expect(err?.isAggregate).toBe(true);
		expect(err?.message).toBe("Multiple failed");
	});

	test("Outcome.all() collects multiple errors", () => {
		const [, err] = Outcome.all([
			Outcome.ok(1),
			Outcome.err<number>("Error A"),
			Outcome.err<number>("Error B"),
		]).toTuple();
		expect(err?.errors).toHaveLength(2);
	});

	test("Outcome.all() with empty array returns ok([])", () => {
		expect(Outcome.all([]).toTuple()[0]).toEqual([]);
	});

	test("Outcome.any() returns first success", () => {
		const result = Outcome.any([
			Outcome.err<number>("First failed"),
			Outcome.ok(42),
			Outcome.ok(100),
		]);
		expect(result.toTuple()[0]).toBe(42);
	});

	test("Outcome.any() fails when all fail", () => {
		const [, err] = Outcome.any([
			Outcome.err<number>("Error 1"),
			Outcome.err<number>("Error 2"),
		]).toTuple();
		expect(err?.isAggregate).toBe(true);
	});

	test("Outcome.any() with empty array returns error", () => {
		const [, err] = Outcome.any([]).toTuple();
		expect(err?.message).toBe("No outcomes provided");
		expect(err?.code).toBe("EMPTY_INPUT");
	});
});

// ══════════════════════════════════════════════════════════════════════════════
// 7. Migration from throwing
// ══════════════════════════════════════════════════════════════════════════════

describe("Migration from throwing", () => {
	test("before/after pattern: throwing vs Outcome", () => {
		// "Before" pattern: function that throws
		function getUserThrowing(id: string): { name: string } {
			const db = new Map([["123", { name: "Alice" }]]);
			const user = db.get(id);
			if (!user) throw new Error("Not found");
			return user;
		}

		// "After" pattern: function that returns Outcome
		function getUserOutcome(id: string): Outcome<{ name: string }> {
			return Outcome.from(() => {
				const db = new Map([["123", { name: "Alice" }]]);
				const user = db.get(id);
				if (!user) return Err.from("Not found", "NOT_FOUND");
				return user;
			});
		}

		// Throwing version: success
		expect(getUserThrowing("123")).toEqual({ name: "Alice" });

		// Throwing version: error
		expect(() => getUserThrowing("999")).toThrow("Not found");

		// Outcome version: success
		const [user, err] = getUserOutcome("123").toTuple();
		expect(err).toBeNull();
		expect(user).toEqual({ name: "Alice" });

		// Outcome version: error
		const [user2, err2] = getUserOutcome("999").toTuple();
		expect(user2).toBeNull();
		expect(err2?.message).toBe("Not found");
		expect(err2?.hasCode("NOT_FOUND")).toBe(true);
	});

	test("class docblock: from() with conditional success/error", () => {
		const shouldSucceed = false;
		const outcome = Outcome.from(() => {
			if (shouldSucceed) return 42;
			return Err.from("Bad luck");
		});

		const [value, err] = outcome.toTuple();
		expect(value).toBeNull();
		expect(err?.message).toBe("Bad luck");
	});

	test("toString() for success and error", () => {
		expect(Outcome.ok(42).toString()).toBe("Outcome.ok(42)");
		expect(Outcome.err("Failed").toString()).toBe(
			"Outcome.err([ERROR] Failed)",
		);
	});

	test("toJSON() round-trip for success outcome", () => {
		const json = JSON.stringify(Outcome.ok({ name: "John" }).toJSON());
		expect(json).toBe('[{"name":"John"},null]');

		const restored = Outcome.fromJSON(JSON.parse(json));
		expect(restored.toTuple()[0]).toEqual({ name: "John" });
	});
});