Skip to content
better-result
Esc
navigateopen⌘Jpreview
On this page

Result codecs

Validate and transform both Result branches across RPC, persistence, queues, and server-action boundaries.

Result.codec builds a two-way boundary from Standard Schema-compatible schemas. Zod, Valibot, ArkType, and other compliant libraries can provide the validators.

Why a codec

A Result object in memory is not proof that JSON or stored data has the same shape. A codec validates both the envelope and selected payload while allowing in-memory and wire representations to differ.

Define both directions

Define four named schemas at the boundary: success and error schemas for values going to the wire, and success and error schemas for values coming from the wire. Keep each schema’s validation and mapping outside the codec declaration.

import { Result } from "better-result";

const UserResultCodec = Result.codec({
  serialize: {
    ok: UserToWireSchema,
    err: DomainErrorToWireSchema,
  },
  deserialize: {
    ok: UserFromWireSchema,
    err: DomainErrorFromWireSchema,
  },
});

The named schemas may validate identical in-memory and wire shapes or map between different representations such as Date and ISO text. Result.codec only assembles those four boundary contracts.

Serialize

const encoded = await UserResultCodec.serialize(Result.ok(user));
// Result<SerializedResult<UserWire, ErrorWire>, ResultSerializationError>

The output envelope is one of:

{ status: "ok", value: okPayload }
{ status: "error", error: errPayload }

A schema issue returns ResultSerializationError with value and issues.

Deserialize unknown input

const decoded = await UserResultCodec.deserialize(inputFromNetwork);
// Result<User, DomainError | ResultDeserializationError>

An invalid envelope or payload returns ResultDeserializationError. Its value is the invalid input; schema failures also include issues.

Unsafe convenience methods

When you own both producer and consumer and version their schemas together, the unsafe variants are often the simpler choice. A codec validation error then usually means the shared contract is broken, so the unsafe methods remove the codec-error handling layer and its associated unwrapping or translation boilerplate:

const envelope = await UserResultCodec.serializeUnsafe(Result.ok(user));
// SerializedResult<UserWire, ErrorWire>

const decoded = await UserResultCodec.deserializeUnsafe(inputFromNetwork);
// Result<User, DomainError>

serializeUnsafe unwraps the serialization Result and panics on ResultSerializationError. deserializeUnsafe panics only on ResultDeserializationError; a valid serialized Err remains a decoded domain Err. Both preserve the selected schema’s synchronous or asynchronous behavior.

Use the safe methods for public, independently versioned, persisted, or otherwise untrusted boundaries where contract mismatch is an expected condition that the caller should handle.

Sync and async schemas

Each selected schema controls whether that operation returns Result, Promise<Result>, or a union when the branch is unknown. await accepts both and is the simplest uniform boundary style:

const decoded = await codec.deserialize(input);

A schema that throws or rejects violates Standard Schema validation behavior and causes Panic.

Undefined payloads

JSON omits properties with undefined. The codec accepts { status: "ok" } and { status: "error" }, then passes undefined to the selected payload schema. A void/undefined schema may accept it; other schemas return ResultDeserializationError.

Was this page helpful?