Skip to main content

Overview

The serialization API allows you to convert Result instances to plain JavaScript objects for network transfer, RPC calls, or server actions, then reconstruct them back into proper Ok or Err instances. This is essential for:
  • Next.js Server Actions
  • tRPC procedures
  • API responses
  • Any scenario where Result instances cross process boundaries

Result.serialize

Converts a Result instance into a plain object that can be safely serialized to JSON.

Signature

Result.serialize<T, E>(result: Result<T, E>): SerializedResult<T, E>
result
Result<T, E>
required
The Result instance to serialize
Return Type
SerializedResult<T, E>
A plain object representing the Result:
  • { status: "ok", value: T } for Ok
  • { status: "error", error: E } for Err

Usage Examples

import { Result } from "better-result";

const success = Result.ok({ id: 1, name: "Alice" });
const serialized = Result.serialize(success);
console.log(serialized);
// { status: "ok", value: { id: 1, name: "Alice" } }

const failure = Result.err({ code: "NOT_FOUND", message: "User not found" });
const serializedErr = Result.serialize(failure);
console.log(serializedErr);
// { status: "error", error: { code: "NOT_FOUND", message: "User not found" } }

Result.deserialize

Reconstructs a Result instance from a serialized plain object. Returns Err<ResultDeserializationError> if the input is invalid.

Signature

Result.deserialize<T, E>(
  value: unknown
): Result<T, E | ResultDeserializationError>
value
unknown
required
The value to deserialize (typically from JSON)
Return Type
Result<T, E | ResultDeserializationError>
  • Ok<T, E> if value is a valid serialized Ok
  • Err<T, E> if value is a valid serialized Err
  • Err<T, ResultDeserializationError> if value is invalid

Usage Examples

import { Result } from "better-result";

const serialized = { status: "ok", value: { id: 1, name: "Alice" } };
const result = Result.deserialize<{ id: number; name: string }, never>(serialized);

if (result.isOk()) {
  console.log(result.value); // { id: 1, name: "Alice" }
}
Result.deserialize is type-safe but cannot validate the actual runtime shape of T or E. Consider using a schema validator like Zod for runtime validation of the payload.

SerializedResult Type

The plain object structure representing a serialized Result.

Type Definition

type SerializedResult<T, E> = SerializedOk<T> | SerializedErr<E>;

interface SerializedOk<T> {
  status: "ok";
  value: T;
}

interface SerializedErr<E> {
  status: "error";
  error: E;
}
SerializedOk<T>
object
Represents a serialized success result
SerializedErr<E>
object
Represents a serialized error result

Usage Example

import type { SerializedResult } from "better-result";

// Type annotation for API responses
type ApiResponse = SerializedResult<
  { id: string; name: string },
  { code: string; message: string }
>;

const response: ApiResponse = {
  status: "ok",
  value: { id: "123", name: "Alice" },
};

ResultDeserializationError

Error returned when Result.deserialize receives invalid input.

Class Definition

class ResultDeserializationError extends TaggedError("ResultDeserializationError")<{
  message: string;
  value: unknown;
}>()
_tag
"ResultDeserializationError"
required
Tagged error discriminator
message
string
required
Error message describing the deserialization failure
value
unknown
required
The invalid value that failed to deserialize

Type Guard

ResultDeserializationError.is(value: unknown): value is ResultDeserializationError
Static type guard method to check if a value is a ResultDeserializationError instance.

Usage Examples

import { Result, ResultDeserializationError } from "better-result";

const result = Result.deserialize(unknownData);

if (Result.isError(result)) {
  if (ResultDeserializationError.is(result.error)) {
    console.error("Invalid serialized Result:", result.error.value);
  } else {
    console.error("Application error:", result.error);
  }
}

Complete Example: Server Action + Client

"use server";

import { Result } from "better-result";

class NotFoundError {
  readonly _tag = "NotFoundError";
  constructor(public id: string) {}
}

class DbError {
  readonly _tag = "DbError";
  constructor(public message: string) {}
}

export async function fetchUserData(userId: string) {
  const result = await Result.gen(async function* () {
    const user = yield* await Result.tryPromise({
      try: () => db.user.findUnique({ where: { id: userId } }),
      catch: () => new NotFoundError(userId),
    });

    if (!user) {
      return Result.err(new NotFoundError(userId));
    }

    const posts = yield* await Result.tryPromise({
      try: () => db.post.findMany({ where: { userId } }),
      catch: (err) => new DbError(String(err)),
    });

    return Result.ok({ user, posts });
  });

  // Serialize for client transfer
  return Result.serialize(result);
}

Best Practices

Always validate deserialized data - Result.deserialize only validates the Result structure, not the actual payload types. Use schema validators like Zod for production applications.
import { Result } from "better-result";
import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

const result = Result.deserialize(data)
  .andThen((value) =>
    Result.try({
      try: () => UserSchema.parse(value),
      catch: (err) => new ValidationError(String(err)),
    })
  );
Serialize at boundaries - Only serialize Results when crossing process boundaries (server actions, API routes). Keep them as proper instances within the same process.
Type consistency - Ensure the generic types used in Result.deserialize match those used in Result.serialize on the server side.

See Also