Effect 4 Schema classes, typed errors and retries, tested with @effect/vitest: working versions (tested 2026-10-15)
Effect 4 Schema.Class models with checks, brands, FiniteFromString and fromJsonString for decode and encode, TaggedError failures, and Effect.retry with exponential backoff verified on the TestClock using @effect/vitest.
passed5 passed · 0 failed2026-10-15 14:12 UTC- Status
- passed
- Last passed
- 2026-10-15 14:12 UTC
- Tests
- 5 / 5
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38748988915
The code
Exactly the files that passed. Your agent can pull them with get_recipe("effect-schema-vitest") (install), or see them on GitHub.
{
"name": "effect-schema-vitest",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
},
"dependencies": {
"effect": "4.0.5"
},
"devDependencies": {
"@effect/vitest": "4.0.5",
"vitest": "5.0.4",
"@types/node": "26.6.5"
}
}
import { Effect, Schedule, Schema } from "effect";
/** Wire format: JSON with string numbers, as many legacy APIs send them. */
export class LineItem extends Schema.Class<LineItem>("LineItem")({
sku: Schema.String.check(Schema.isPattern(/^[A-Z]{3}-\d{3}$/)),
qty: Schema.Int.check(Schema.isGreaterThan(0)),
// FiniteFromString rejects "x"; NumberFromString would decode it to NaN
unitCents: Schema.FiniteFromString,
}) {
get totalCents() { return this.qty * this.unitCents; }
}
export class Order extends Schema.Class<Order>("Order")({
id: Schema.String.pipe(Schema.brand("OrderId")),
customer: Schema.Trim.check(Schema.isMinLength(1)),
status: Schema.Literals(["pending", "paid", "shipped"]),
items: Schema.Array(LineItem).check(Schema.isMinLength(1)),
note: Schema.optionalKey(Schema.String),
}) {
get totalCents() { return this.items.reduce((s, i) => s + i.totalCents, 0); }
}
/** Decodes a raw JSON string straight into an Order instance. */
export const OrderFromJson = Schema.fromJsonString(Order);
export class InvalidOrder extends Schema.TaggedError<InvalidOrder>()("InvalidOrder", { message: Schema.String }) {}
export class UpstreamUnavailable extends Schema.TaggedError<UpstreamUnavailable>()("UpstreamUnavailable", { attempt: Schema.Number }) {}
export const parseOrder = (json: string) =>
// errors: "all" collects every issue instead of stopping at the first one
Schema.decodeUnknownEffect(OrderFromJson)(json, { errors: "all" }).pipe(Effect.mapError((e) => new InvalidOrder({ message: e.message })));
export const encodeOrder = (o: Order) => Schema.encodeEffect(OrderFromJson)(o);
/**
* Fetches an order through a flaky upstream, retrying UpstreamUnavailable with exponential
* backoff (100ms, 200ms, ...) up to 3 retries. InvalidOrder is never retried.
*/
export const fetchOrder = (fetchRaw: (attempt: number) => Effect.Effect<string, UpstreamUnavailable>) => {
let attempt = 0;
return Effect.suspend(() => fetchRaw(++attempt)).pipe(
Effect.retry({ schedule: Schedule.exponential("100 millis"), times: 3 }),
Effect.flatMap(parseOrder),
);
};
import { assert, describe, it } from "@effect/vitest";
import { Effect, Exit, Fiber } from "effect";
import { TestClock } from "effect/testing";
import { parseOrder, encodeOrder, fetchOrder, Order, UpstreamUnavailable } from "../src/orders.ts";
const raw = JSON.stringify({ id: "o-1", customer: " Ada ", status: "paid", items: [{ sku: "HAT-001", qty: 2, unitCents: "2499" }, { sku: "GLV-010", qty: 1, unitCents: "899" }] });
describe("Effect Schema orders", () => {
it.effect("decodes JSON into class instances with transformations applied", () =>
Effect.gen(function* () {
const order = yield* parseOrder(raw);
assert.isTrue(order instanceof Order);
assert.strictEqual(order.customer, "Ada");
assert.strictEqual(order.items[0].unitCents, 2499);
assert.strictEqual(order.totalCents, 2 * 2499 + 899);
assert.isFalse("note" in order);
}));
it.effect("encodes back to the wire format", () =>
Effect.gen(function* () {
const order = yield* parseOrder(raw);
const json = JSON.parse(yield* encodeOrder(order));
assert.strictEqual(json.items[0].unitCents, "2499");
assert.strictEqual(json.customer, "Ada");
}));
it.effect("invalid input fails with a typed InvalidOrder error", () =>
Effect.gen(function* () {
const bad = JSON.stringify({ id: "o-2", customer: " ", status: "lost", items: [{ sku: "bad", qty: 0, unitCents: "x" }] });
const exit = yield* Effect.exit(parseOrder(bad));
assert.isTrue(Exit.isFailure(exit));
const err = yield* Effect.flip(parseOrder(bad));
assert.strictEqual(err._tag, "InvalidOrder");
for (const field of ["customer", "status", "sku", "qty", "unitCents"]) assert.include(err.message, field);
const notJson = yield* Effect.flip(parseOrder("{oops"));
assert.strictEqual(notJson._tag, "InvalidOrder");
}));
it.effect("retries the upstream with backoff on the TestClock", () =>
Effect.gen(function* () {
const calls: number[] = [];
const upstream = (attempt: number) => {
calls.push(attempt);
return attempt < 3 ? Effect.fail(new UpstreamUnavailable({ attempt })) : Effect.succeed(raw);
};
const fiber = yield* Effect.forkChild(fetchOrder(upstream));
yield* TestClock.adjust("100 millis");
yield* TestClock.adjust("200 millis");
const order = yield* Fiber.join(fiber);
assert.deepStrictEqual(calls, [1, 2, 3]);
assert.strictEqual(order.status, "paid");
}));
it.effect("gives up after the retry budget with the last upstream error", () =>
Effect.gen(function* () {
const fiber = yield* Effect.forkChild(Effect.flip(fetchOrder((attempt) => Effect.fail(new UpstreamUnavailable({ attempt })))));
yield* TestClock.adjust("10 seconds");
const err = yield* Fiber.join(fiber);
assert.strictEqual(err._tag, "UpstreamUnavailable");
assert.strictEqual((err as UpstreamUnavailable).attempt, 4);
}));
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Astro 7 component rendering tests with the Container API (Vitest)
Render Astro components to HTML in Vitest with the Container API and getViteConfig, checking props, slots and conditional markup.
AWS SDK v3 DynamoDB document client and SQS events, tested with Vitest matchers
A single-table DynamoDB order service on @aws-sdk/lib-dynamodb with conditional puts, begins_with queries and optimistic locking, publishing SQS events, tested offline with aws-sdk-client-mock and aws-sdk-client-mock-vitest matchers.
Hono + Drizzle ORM + SQLite REST API (Vitest)
CRUD REST API in Hono backed by Drizzle ORM on better-sqlite3, with Zod validation, tested with Vitest and app.request().
Hono + Valibot request validation (Vitest)
Validate JSON bodies and query strings in Hono with Valibot schemas via @hono/valibot-validator, with custom error responses, tested with Vitest.
All effect recipesAll @effect/vitest recipesAll vitest recipes