High-level transactions
The SDKs provide saga helpers for workflows that need compensation actions after a domain failure.
Each host call remains durable on its own. A saga registers compensations for successful steps and runs them in reverse order when a later step fails. Compensation is a new external action, not deletion of the earlier effect.
Sagas do not provide atomicity or isolation. Earlier calls and their compensations remain in history, and other tasks can observe effects before compensation runs.
The SDK provides support for two different types of transactions:
- fallible transactions compensate failures and report the outcome
- infallible transactions compensate failures and ask the host policy before appending a new attempt
Fallible transactions
A workflow can use compensations to counter earlier successful operations when a later operation fails. For example, a compensation can request a refund after a charge. It does not erase the charge or hide it from other tasks, and the compensation itself can fail.
A fallible transaction deals with domain errors. Successful steps register compensations. Propagating a domain error out of the transaction body triggers these compensations in reverse order; handling the error inside the body does not automatically trigger rollback. The failing step has no registered compensation. TypeScript, Rust, Scala and Go stop at the first compensation error and report partial rollback. Effect’s withFallibleCompensation continues the remaining compensations and reports the first compensation error.
TypeScript
In TypeScript, a fallible saga can be executed using the async fallibleSaga function, by
passing an async callback that can execute operations on the open transaction (see below).
Infallible transactions
After each failed, fully compensated infallible attempt, the SDK resolves a host policy with verb
saga, noun the agent URN, and attempt/error-kind properties. The host’s next-retry-delay
uses the existing evaluator and an opaque binary continuation. SDKs carry that state unchanged
and use durable clocks and timers. No matching policy selects the agent default. Unbounded
policies are rejected rather than silently capped. Count/time/never exhaustion returns a typed
guest failure; selector/evaluator errors retain the original failure too.
New attempts append calls and retain failed attempts and compensations. Bounds keep attempts
finite; they do not rewind history. Reconstruction repairs an incomplete call at its original
Start, without discarding sibling history. TS, Effect, Scala and Go compensate caught failures
in reverse order. Fallible helpers propagate the original exception/panic only after complete
rollback; partial rollback retains both failures. Effect external cancellation stops retries and
preserves original and compensation evidence in Cause; self-interruption is policy-retryable.
Rust WASM panic aborts without compensation. An uncatchable deterministic guest trap permanently
fails the invocation; generic infrastructure retry handling is unchanged. MoonBit exposes the
retry API but has no saga helper.
TypeScript
In TypeScript, an infallible saga can be executed using the async infallibleSaga function, by
passing an async callback that can execute operations on the open transaction (see below).
Operations
Both transaction types require the definition of operations.
It is defined with the following interface:
TypeScript
/**
* Represents a compensable step of the transaction.
*
* Implement this interface and use it within a saga block.
* Compensable steps can also be constructed with `compensable`.
*/
export interface Compensable<In, Out, Err> {
/**
* The action to execute.
* @param input - The input to the operation.
* @returns A promise resolving to the result of the operation.
*/
execute(input: In): Promise<Result<Out, Err>>
/**
* Compensation to perform in case of failure.
* Compensations should not throw errors.
* @param input - The input to the operation.
* @param result - The result of the operation.
* @returns A promise resolving to the result of the compensation.
*/
compensate(input: In, result: Out): Promise<Result<void, Err>>
}There are two ways to define a compensable step:
- Implement the
Compensableinterface manually - Use the
compensablefunction to create one from a pair of closures
export function compensable<In, Out, Err>(
execute: (input: In) => Promise<Result<Out, Err>>,
compensate: (input: In, result: Out) => Promise<Result<void, Err>>,
): Compensable<In, Out, Err>Executing operations
The defined operations can be executed in fallible or infallible mode:
TypeScript
import { compensable, fallibleSaga, infallibleSaga, Result } from "@golemcloud/golem-ts-sdk"
// example operation with compensation
const op = compensable<number, string, string>(
async (idx) => {
// the operation / side effect
return Result.ok("id-" + idx)
},
async (idx, id) => {
// compensation
console.log(`reverting ${id}, ${idx}`)
return Result.ok(undefined)
}
)
// With fallibleSaga, errors are propagated using Result.
const resultFallible = await fallibleSaga(async tx => {
const firstId = await tx.execute(op, 1)
if (firstId.isErr()) return firstId
const secondId = await tx.execute(op, 2)
if (secondId.isErr()) return secondId
return Result.ok([firstId.val, secondId.val])
})
const resultInfallible = await infallibleSaga(async tx => {
const firstId = await tx.execute(op, 1)
const secondId = await tx.execute(op, 2)
return [firstId, secondId]
})