Skip to content

Instantly share code, notes, and snippets.

@dbrxnds
Created January 16, 2026 08:28
Show Gist options
  • Select an option

  • Save dbrxnds/b052ba3df36115f650c537e7d5de3ffd to your computer and use it in GitHub Desktop.

Select an option

Save dbrxnds/b052ba3df36115f650c537e7d5de3ffd to your computer and use it in GitHub Desktop.
Effect + tRPC ProcedureResult utility.
import { Cause, Data, Effect, Predicate, type Types } from "effect"
declare const ProcedureResultTaggedErrorBrand: unique symbol
export interface TaggedError<
Tag extends string = string,
Serialized extends { readonly _tag: Tag } = { readonly _tag: Tag },
> {
readonly _tag: Tag
readonly [ProcedureResultTaggedErrorBrand]: Serialized
toJSON(): Serialized
}
export type FailureResult<E extends TaggedError> = {
readonly _type: "Failure"
} & E[typeof ProcedureResultTaggedErrorBrand]
/**
* Creates an error class that indicates it can safely be sent to the client via tRPC.
* Works like `Data.TaggedError` but adds type branding for use with `toResult`.
*
* @example
* ```ts
* class MissingPermissionError extends ProcedureResult.TaggedError("MissingPermissionError")<{
* permission: string
* }> {}
*
* // No fields:
* class InternalServerError extends ProcedureResult.TaggedError("InternalServerError") {}
* ```
*/
/* eslint-disable @typescript-eslint/no-empty-object-type */
export const TaggedError = <Tag extends string>(
tag: Tag,
): new <A extends Record<string, unknown> = {}>(
...args: Types.Equals<A, {}> extends true
? []
: [{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }]
) => Cause.YieldableError & { readonly _tag: Tag } & Readonly<A> &
TaggedError<Tag, { readonly _tag: Tag } & Readonly<A>> => {
return Data.TaggedError(tag) as never
}
export class InternalServerError extends TaggedError("InternalServerError") {}
export type SuccessResult<A> = { readonly _type: "Success"; readonly _tag: "Success"; value: A }
export type ProcedureResult<A, E extends TaggedError> = SuccessResult<A> | FailureResult<E>
export type Success<T extends ProcedureResult<unknown, TaggedError>> = [T] extends [
ProcedureResult<infer A, TaggedError>,
]
? SuccessResult<A>
: never
export type Failure<T extends ProcedureResult<unknown, TaggedError>> = [T] extends [
ProcedureResult<object, infer E>,
]
? FailureResult<E>
: never
export function isSuccess<A, E extends TaggedError>(
result: Nullish<ProcedureResult<A, E>>,
): result is SuccessResult<A> {
return Predicate.isNotNullable(result) && result._type === "Success"
}
export function isFailure<A, E extends TaggedError>(
result: Nullish<ProcedureResult<A, E>>,
): result is FailureResult<E> {
return Predicate.isNotNullable(result) && result._type === "Failure"
}
export function match<A, E extends TaggedError, T1, T2>(
result: ProcedureResult<A, E>,
options: {
onSuccess: (data: SuccessResult<A>["value"]) => T1
onFailure: (error: FailureResult<E>) => T2
},
): T1 | T2 {
return isSuccess(result) ? options.onSuccess(result.value) : options.onFailure(result)
}
/**
* Converts an effect to a `ProcedureResult`.
* Only `ProcedureResult.TaggedError` is supported as an error type. See `ProcedureResult.TaggedError` for more information.
*
* @example
* ```ts
* .pipe(
* ...effects,
* ProcedureResult.toResult,
* )
* ```
*
*/
export function toResult<A, E extends TaggedError, R>(
effect: Effect.Effect<A, E, R>,
): Effect.Effect<ProcedureResult<A, E | InternalServerError>, never, R> {
return effect.pipe(
Effect.matchCause({
onSuccess: (value) => ({ _type: "Success", _tag: "Success", value }),
onFailure: (cause) => {
const error = Cause.isFailType(cause)
? cause.error.toJSON()
: new InternalServerError().toJSON()
return {
_type: "Failure",
...(typeof error === "object" && error),
} as FailureResult<E | InternalServerError>
},
}),
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment