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 >
The Result instance to serialize
A plain object representing the Result:
{ status: "ok", value: T } for Ok
{ status: "error", error: E } for Err
Usage Examples
Basic Serialization
Next.js Server Action
tRPC Procedure
API Route
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 >
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
Basic Deserialization
Client-Side (Next.js)
tRPC Client
Error Handling
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 ;
}
Represents a serialized success result Discriminator field with literal value “ok”
Represents a serialized error result Discriminator field with literal value “error”
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
Error message describing the deserialization failure
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
Error Detection
Pattern Matching
With Error Normalization
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
actions.ts (Server)
component.tsx (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