Skip to Content
DevelopTransactions

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.

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.

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:

/** * 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:

  1. Implement the Compensable interface manually
  2. Use the compensable function 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:

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] })