Skip to content

Software Transactional Memory (STM)

Managing shared state in concurrent applications is notoriously difficult. Traditional approaches often involve complex locking mechanisms that are prone to deadlocks, race conditions, and are hard to compose.

Effect provides a powerful solution: Software Transactional Memory (STM). STM simplifies concurrent programming by allowing you to treat complex operations on shared memory as atomic transactions.

Key Principles of STM:

  • Atomicity: All operations within an STM transaction complete successfully together, or none of them do. The system never ends up in a partially updated state.
  • Consistency: Transactions ensure that the shared state always remains consistent according to your defined rules (invariants).
  • Isolation: The intermediate states of a transaction are invisible to other concurrent transactions until the transaction successfully commits.
  • Composability: STM operations are represented as effects (STM<R, E, A>) which can be easily combined and composed, just like regular Effects.

STM in Effect revolves around two main types:

A TRef<A> (Transactional Reference) is a mutable reference to a value of type A, but it can only be accessed or modified within an STM transaction. Think of it as a container for shared state that is managed by the STM system.

You create a TRef using TRef.make:

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
import TRef
TRef
} from "effect";
// Creates a TRef initialized with the value 0
const
const createCounter: STM<TRef.TRef<number>, never, never>
createCounter
=
import TRef
TRef
.
const make: <number>(value: number) => STM<TRef.TRef<number>, never, never>

@since2.0.0

make
(0)
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const runPromise: <TRef.TRef<number>, never>(effect: Effect.Effect<TRef.TRef<number>, never, never>, options?: {
readonly signal?: AbortSignal;
} | undefined) => Promise<...>

Executes an effect and returns the result as a Promise.

Details

This function runs an effect and converts its result into a Promise. If the effect succeeds, the Promise will resolve with the successful result. If the effect fails, the Promise will reject with an error, which includes the failure details of the effect.

The optional options parameter allows you to pass an AbortSignal for cancellation, enabling more fine-grained control over asynchronous tasks.

When to Use

Use this function when you need to execute an effect and work with its result in a promise-based system, such as when integrating with third-party libraries that expect Promise results.

Example (Running a Successful Effect as a Promise)

import { Effect } from "effect"
Effect.runPromise(Effect.succeed(1)).then(console.log)
// Output: 1

Example (Handling a Failing Effect as a Rejected Promise)

import { Effect } from "effect"
Effect.runPromise(Effect.fail("my error")).catch(console.error)
// Output:
// (FiberFailure) Error: my error

@seerunPromiseExit for a version that returns an Exit type instead of rejecting.

@since2.0.0

runPromise
(
const createCounter: STM<TRef.TRef<number>, never, never>
createCounter
).
Promise<TRef<number>>.then<void, never>(onfulfilled?: ((value: TRef.TRef<number>) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<...>

Attaches callbacks for the resolution and/or rejection of the Promise.

@paramonfulfilled The callback to execute when the Promise is resolved.

@paramonrejected The callback to execute when the Promise is rejected.

@returnsA Promise for the completion of which ever callback is executed.

then
((
counterRef: TRef.TRef<number>
counterRef
) => {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
('TRef created:',
counterRef: TRef.TRef<number>
counterRef
)
})

An STM<R, E, A> represents a description of a transactional computation. It’s similar to Effect<R, E, A> but specifically designed to work with TRefs within a transaction.

  • A: The success value type of the transaction.
  • E: The potential error type if the transaction fails.
  • R: The environment requirements for the transaction.

Operations on TRefs, like getting or setting their value, return STM effects:

import {
import STM
STM
,
import TRef
TRef
} from "effect";
// Create a TRef for demonstration
const
const createCounter: STM.STM<TRef.TRef<number>, never, never>
createCounter
=
import TRef
TRef
.
const make: <number>(value: number) => STM.STM<TRef.TRef<number>, never, never>

@since2.0.0

make
(0);
// For demonstration, we'll pretend we have a counter
declare const
const counter: TRef.TRef<number>
counter
:
import TRef
TRef
.
interface TRef<in out A>

A TRef<A> is a purely functional description of a mutable reference that can be modified as part of a transactional effect. The fundamental operations of a TRef are set and get. set transactionally sets the reference to a new value. get gets the current value of the reference.

NOTE: While TRef<A> provides the transactional equivalent of a mutable reference, the value inside the TRef should be immutable.

@since2.0.0

@since2.0.0

TRef
<number>;
// Get the current value inside the TRef
const
const getCount: STM.STM<number, never, never>
getCount
=
import TRef
TRef
.
const get: <number>(self: TRef.TRef<number>) => STM.STM<number, never, never>

@since2.0.0

get
(
const counter: TRef.TRef<number>
counter
);
// Set the value inside the TRef to 5
const
const setCount: STM.STM<void, never, never>
setCount
=
import TRef
TRef
.
const set: <number>(self: TRef.TRef<number>, value: number) => STM.STM<void> (+1 overload)

@since2.0.0

set
(
const counter: TRef.TRef<number>
counter
, 5);
// Update the value based on the current value
const
const incrementCount: STM.STM<number, never, never>
incrementCount
=
import TRef
TRef
.
const updateAndGet: <number>(self: TRef.TRef<number>, f: (a: number) => number) => STM.STM<number, never, never> (+1 overload)

@since2.0.0

updateAndGet
(
const counter: TRef.TRef<number>
counter
, (
n: number
n
) =>
n: number
n
+ 1);
// or just update without returning the new value:
const
const incrementCountVoid: STM.STM<void, never, never>
incrementCountVoid
=
import TRef
TRef
.
const update: <number>(self: TRef.TRef<number>, f: (a: number) => number) => STM.STM<void> (+1 overload)

@since2.0.0

update
(
const counter: TRef.TRef<number>
counter
, (
n: number
n
) =>
n: number
n
+ 1);

Notice that these operations describe what should happen in a transaction, but they don’t execute it immediately.

To actually execute the operations described by an STM effect, you need to commit it. The STM.commit function transforms an STM<R, E, A> into a regular Effect<R, E, A>.

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
import STM
STM
,
import TRef
TRef
} from "effect";
const
const program: Effect.Effect<void, never, never>
program
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const gen: <YieldWrap<Effect.Effect<void, never, never>>, void>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Effect.Effect<void, never, never>>, void, never>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

gen
(function* () {
// Create a TRef within the Effect context
const
const counter: TRef.TRef<number>
counter
= yield*
import TRef
TRef
.
const make: <number>(value: number) => STM.STM<TRef.TRef<number>, never, never>

@since2.0.0

make
(10);
// Describe an STM transaction: get the value and add 5
const
const transaction: STM.STM<number, never, never>
transaction
=
import STM
STM
.
const gen: <unknown, YieldWrap<STM.STM<void, never, never>>, number>(...args: [self: unknown, body: (this: unknown, resume: STM.Adapter) => Generator<YieldWrap<STM.STM<void, never, never>>, number, never>] | [body: ...]) => STM.STM<...>

@since2.0.0

gen
(function* () {
const
const current: number
current
= yield*
import TRef
TRef
.
const get: <number>(self: TRef.TRef<number>) => STM.STM<number, never, never>

@since2.0.0

get
(
const counter: TRef.TRef<number>
counter
);
const
const newValue: number
newValue
=
const current: number
current
+ 5;
yield*
import TRef
TRef
.
const set: <number>(self: TRef.TRef<number>, value: number) => STM.STM<void> (+1 overload)

@since2.0.0

set
(
const counter: TRef.TRef<number>
counter
,
const newValue: number
newValue
);
return
const newValue: number
newValue
;
});
// Commit the transaction to turn it into an Effect
const
const effectfulTransaction: Effect.Effect<number, never, never>
effectfulTransaction
=
import STM
STM
.
const commit: <number, never, never>(self: STM.STM<number, never, never>) => Effect.Effect<number, never, never>

Commits this transaction atomically.

@since2.0.0

commit
(
const transaction: STM.STM<number, never, never>
transaction
);
// Run the resulting Effect
const
const result: number
result
= yield*
const effectfulTransaction: Effect.Effect<number, never, never>
effectfulTransaction
;
yield*
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const log: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

Logs one or more messages or error causes at the current log level.

Details

This function provides a simple way to log messages or error causes during the execution of your effects. By default, logs are recorded at the INFO level, but this can be adjusted using other logging utilities (Logger.withMinimumLogLevel). Multiple items, including Cause instances, can be logged in a single call. When logging Cause instances, detailed error information is included in the log output.

The log output includes useful metadata like the current timestamp, log level, and fiber ID, making it suitable for debugging and tracking purposes. This function does not interrupt or alter the effect's execution flow.

Example

import { Cause, Effect } from "effect"
const program = Effect.log(
"message1",
"message2",
Cause.die("Oh no!"),
Cause.die("Oh uh!")
)
Effect.runFork(program)
// Output:
// timestamp=... level=INFO fiber=#0 message=message1 message=message2 cause="Error: Oh no!
// Error: Oh uh!"

@since2.0.0

log
(`Transaction committed. New value: ${
const result: number
result
}`); // Output: New value: 15
// Verify the value outside the transaction (requires another commit)
const
const finalValue: number
finalValue
= yield*
import STM
STM
.
const commit: <number, never, never>(self: STM.STM<number, never, never>) => Effect.Effect<number, never, never>

Commits this transaction atomically.

@since2.0.0

commit
(
import TRef
TRef
.
const get: <number>(self: TRef.TRef<number>) => STM.STM<number, never, never>

@since2.0.0

get
(
const counter: TRef.TRef<number>
counter
));
yield*
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const log: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

Logs one or more messages or error causes at the current log level.

Details

This function provides a simple way to log messages or error causes during the execution of your effects. By default, logs are recorded at the INFO level, but this can be adjusted using other logging utilities (Logger.withMinimumLogLevel). Multiple items, including Cause instances, can be logged in a single call. When logging Cause instances, detailed error information is included in the log output.

The log output includes useful metadata like the current timestamp, log level, and fiber ID, making it suitable for debugging and tracking purposes. This function does not interrupt or alter the effect's execution flow.

Example

import { Cause, Effect } from "effect"
const program = Effect.log(
"message1",
"message2",
Cause.die("Oh no!"),
Cause.die("Oh uh!")
)
Effect.runFork(program)
// Output:
// timestamp=... level=INFO fiber=#0 message=message1 message=message2 cause="Error: Oh no!
// Error: Oh uh!"

@since2.0.0

log
(`Final value in TRef: ${
const finalValue: number
finalValue
}`); // Output: Final value in TRef: 15
});
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const runPromise: <void, never>(effect: Effect.Effect<void, never, never>, options?: {
readonly signal?: AbortSignal;
} | undefined) => Promise<void>

Executes an effect and returns the result as a Promise.

Details

This function runs an effect and converts its result into a Promise. If the effect succeeds, the Promise will resolve with the successful result. If the effect fails, the Promise will reject with an error, which includes the failure details of the effect.

The optional options parameter allows you to pass an AbortSignal for cancellation, enabling more fine-grained control over asynchronous tasks.

When to Use

Use this function when you need to execute an effect and work with its result in a promise-based system, such as when integrating with third-party libraries that expect Promise results.

Example (Running a Successful Effect as a Promise)

import { Effect } from "effect"
Effect.runPromise(Effect.succeed(1)).then(console.log)
// Output: 1

Example (Handling a Failing Effect as a Rejected Promise)

import { Effect } from "effect"
Effect.runPromise(Effect.fail("my error")).catch(console.error)
// Output:
// (FiberFailure) Error: my error

@seerunPromiseExit for a version that returns an Exit type instead of rejecting.

@since2.0.0

runPromise
(
const program: Effect.Effect<void, never, never>
program
);

STM.commit ensures that all operations within the transaction happen atomically. If any part fails, or if there’s a conflict with another concurrent transaction, the whole transaction is rolled back, and the STM runtime might retry it.

Let’s model a common concurrency problem: transferring money between two bank accounts. We want to ensure that the debit from one account and the credit to another happen atomically – money should neither be created nor destroyed, even if many transfers happen concurrently.

import {
import STM
STM
,
import TRef
TRef
,
import Data
Data
} from "effect";
// Custom error using Data.Tagged
class
class InsufficientFundsError
InsufficientFundsError
extends
import Data
Data
.
const TaggedError: <"InsufficientFundsError">(tag: "InsufficientFundsError") => new <A>(args: Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => YieldableError & ... 1 more ... & Readonly<...>

@since2.0.0

TaggedError
(
"InsufficientFundsError",
)<{
readonly
accountId: string
accountId
: string;
readonly
requested: number
requested
: number;
readonly
available: number
available
: number;
}> {}
// Represents a bank account with a balance stored in a TRef
interface
interface Account
Account
{
readonly
Account.id: string
id
: string;
readonly
Account.balance: TRef.TRef<number>
balance
:
import TRef
TRef
.
interface TRef<in out A>

A TRef<A> is a purely functional description of a mutable reference that can be modified as part of a transactional effect. The fundamental operations of a TRef are set and get. set transactionally sets the reference to a new value. get gets the current value of the reference.

NOTE: While TRef<A> provides the transactional equivalent of a mutable reference, the value inside the TRef should be immutable.

@since2.0.0

@since2.0.0

TRef
<number>;
}
/**
* Describes an atomic transfer between two accounts.
* Fails with InsufficientFundsError if the 'from' account lacks funds.
*/
const
const transfer: (from: Account, to: Account, amount: number) => STM.STM<undefined, InsufficientFundsError | Error, never>

Describes an atomic transfer between two accounts. Fails with InsufficientFundsError if the 'from' account lacks funds.

transfer
= (
from: Account
from
:
interface Account
Account
,
to: Account
to
:
interface Account
Account
,
amount: number
amount
: number) =>
import STM
STM
.
const gen: <unknown, YieldWrap<STM.STM<never, Error, never>> | YieldWrap<STM.STM<never, InsufficientFundsError, never>> | YieldWrap<...>, undefined>(...args: [self: ...] | [body: ...]) => STM.STM<...>

@since2.0.0

gen
(function* () {
// Ensure amount is positive
if (
amount: number
amount
<= 0) {
return yield*
import STM
STM
.
const fail: <Error>(error: Error) => STM.STM<never, Error, never>

Fails the transactional effect with the specified error.

@since2.0.0

fail
(
new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("Transfer amount must be positive"),
);
}
// Get the current balance from the 'from' account
const
const fromBalance: number
fromBalance
= yield*
import TRef
TRef
.
const get: <number>(self: TRef.TRef<number>) => STM.STM<number, never, never>

@since2.0.0

get
(
from: Account
from
.
Account.balance: TRef.TRef<number>
balance
);
// Check for sufficient funds
if (
const fromBalance: number
fromBalance
<
amount: number
amount
) {
// Fail the transaction explicitly
return yield*
import STM
STM
.
const fail: <InsufficientFundsError>(error: InsufficientFundsError) => STM.STM<never, InsufficientFundsError, never>

Fails the transactional effect with the specified error.

@since2.0.0

fail
(
new
constructor InsufficientFundsError<{
readonly accountId: string;
readonly requested: number;
readonly available: number;
}>(args: {
readonly accountId: string;
readonly requested: number;
readonly available: number;
}): InsufficientFundsError
InsufficientFundsError
({
accountId: string
accountId
:
from: Account
from
.
Account.id: string
id
,
requested: number
requested
:
amount: number
amount
,
available: number
available
:
const fromBalance: number
fromBalance
,
}),
);
}
// Sufficient funds: proceed with the transfer
// Note: All these TRef operations are part of the *same* atomic transaction
// Debit the 'from' account
yield*
import TRef
TRef
.
const set: <number>(self: TRef.TRef<number>, value: number) => STM.STM<void> (+1 overload)

@since2.0.0

set
(
from: Account
from
.
Account.balance: TRef.TRef<number>
balance
,
const fromBalance: number
fromBalance
-
amount: number
amount
);
// Credit the 'to' account (using update for variety)
yield*
import TRef
TRef
.
const update: <number>(self: TRef.TRef<number>, f: (a: number) => number) => STM.STM<void> (+1 overload)

@since2.0.0

update
(
to: Account
to
.
Account.balance: TRef.TRef<number>
balance
, (
balance: number
balance
) =>
balance: number
balance
+
amount: number
amount
);
// Transaction successful (returns void implicitly)
});

This transfer function returns an STM effect. It describes the atomic steps: check balance, potentially fail, debit sender, credit receiver. Nothing actually happens until we commit this STM.

Now, let’s simulate multiple concurrent transfers using Effect’s concurrency features:

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
import Data
Data
,
import STM
STM
,
import TRef
TRef
,
import Fiber
Fiber
,
import Logger
Logger
,
import LogLevel
LogLevel
} from "effect";
43 collapsed lines
interface
interface Account
Account
{
readonly
Account.id: string
id
: string;
readonly
Account.balance: TRef.TRef<number>
balance
:
import TRef
TRef
.
interface TRef<in out A>

A TRef<A> is a purely functional description of a mutable reference that can be modified as part of a transactional effect. The fundamental operations of a TRef are set and get. set transactionally sets the reference to a new value. get gets the current value of the reference.

NOTE: While TRef<A> provides the transactional equivalent of a mutable reference, the value inside the TRef should be immutable.

@since2.0.0

@since2.0.0

TRef
<number>;
}
class
class InsufficientFundsError
InsufficientFundsError
extends
import Data
Data
.
const TaggedError: <"InsufficientFundsError">(tag: "InsufficientFundsError") => new <A>(args: Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => YieldableError & ... 1 more ... & Readonly<...>

@since2.0.0

TaggedError
(
"InsufficientFundsError",
)<{
readonly
accountId: string
accountId
: string;
readonly
requested: number
requested
: number;
readonly
available: number
available
: number;
}> {}
const
const transfer: (from: Account, to: Account, amount: number) => STM.STM<undefined, InsufficientFundsError | Error, never>
transfer
= (
from: Account
from
:
interface Account
Account
,
to: Account
to
:
interface Account
Account
,
amount: number
amount
: number) =>
import STM
STM
.
const gen: <unknown, YieldWrap<STM.STM<never, Error, never>> | YieldWrap<STM.STM<never, InsufficientFundsError, never>> | YieldWrap<...>, undefined>(...args: [self: ...] | [body: ...]) => STM.STM<...>

@since2.0.0

gen
(function* () {
// Ensure amount is positive
if (
amount: number
amount
<= 0) {
return yield*
import STM
STM
.
const fail: <Error>(error: Error) => STM.STM<never, Error, never>

Fails the transactional effect with the specified error.

@since2.0.0

fail
(
new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("Transfer amount must be positive"),
);
}
// Get the current balance from the 'from' account
const
const fromBalance: number
fromBalance
= yield*
import TRef
TRef
.
const get: <number>(self: TRef.TRef<number>) => STM.STM<number, never, never>

@since2.0.0

get
(
from: Account
from
.
Account.balance: TRef.TRef<number>
balance
);
// Check for sufficient funds
if (
const fromBalance: number
fromBalance
<
amount: number
amount
) {
// Fail the transaction explicitly
return yield*
import STM
STM
.
const fail: <InsufficientFundsError>(error: InsufficientFundsError) => STM.STM<never, InsufficientFundsError, never>

Fails the transactional effect with the specified error.

@since2.0.0

fail
(
new
constructor InsufficientFundsError<{
readonly accountId: string;
readonly requested: number;
readonly available: number;
}>(args: {
readonly accountId: string;
readonly requested: number;
readonly available: number;
}): InsufficientFundsError
InsufficientFundsError
({
accountId: string
accountId
:
from: Account
from
.
Account.id: string
id
,
requested: number
requested
:
amount: number
amount
,
available: number
available
:
const fromBalance: number
fromBalance
,
}),
);
}
// Debit the 'from' account
yield*
import TRef
TRef
.
const set: <number>(self: TRef.TRef<number>, value: number) => STM.STM<void> (+1 overload)

@since2.0.0

set
(
from: Account
from
.
Account.balance: TRef.TRef<number>
balance
,
const fromBalance: number
fromBalance
-
amount: number
amount
);
// Credit the 'to' account
yield*
import TRef
TRef
.
const update: <number>(self: TRef.TRef<number>, f: (a: number) => number) => STM.STM<void> (+1 overload)

@since2.0.0

update
(
to: Account
to
.
Account.balance: TRef.TRef<number>
balance
, (
balance: number
balance
) =>
balance: number
balance
+
amount: number
amount
);
});
const
const program: Effect.Effect<void, InsufficientFundsError | Error, never>
program
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const gen: <YieldWrap<Effect.Effect<void, never, never>> | YieldWrap<Effect.Effect<void[], InsufficientFundsError | Error, never>>, void>(f: (resume: Effect.Adapter) => Generator<...>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

gen
(function* () {
// Create two accounts with initial balances
const
const accountA: Account
accountA
:
interface Account
Account
= {
Account.id: string
id
: 'A',
Account.balance: TRef.TRef<number>
balance
: yield*
import TRef
TRef
.
const make: <number>(value: number) => STM.STM<TRef.TRef<number>, never, never>

@since2.0.0

make
(1000),
}
const
const accountB: Account
accountB
:
interface Account
Account
= {
Account.id: string
id
: 'B',
Account.balance: TRef.TRef<number>
balance
: yield*
import TRef
TRef
.
const make: <number>(value: number) => STM.STM<TRef.TRef<number>, never, never>

@since2.0.0

make
(500),
}
yield*
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const logInfo: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

Logs messages at the INFO log level.

Details

This function logs messages at the INFO level, suitable for general application events or operational messages. INFO logs are shown by default and are commonly used for highlighting normal, non-error operations.

@since2.0.0

logInfo
(`Initial Balances - A: ${yield*
import STM
STM
.
const commit: <number, never, never>(self: STM.STM<number, never, never>) => Effect.Effect<number, never, never>

Commits this transaction atomically.

@since2.0.0

commit
(
import TRef
TRef
.
const get: <number>(self: TRef.TRef<number>) => STM.STM<number, never, never>

@since2.0.0

get
(
const accountA: Account
accountA
.
Account.balance: TRef.TRef<number>
balance
))}, B: ${yield*
import STM
STM
.
const commit: <number, never, never>(self: STM.STM<number, never, never>) => Effect.Effect<number, never, never>

Commits this transaction atomically.

@since2.0.0

commit
(
import TRef
TRef
.
const get: <number>(self: TRef.TRef<number>) => STM.STM<number, never, never>

@since2.0.0

get
(
const accountB: Account
accountB
.
Account.balance: TRef.TRef<number>
balance
))}`)
// Describe transfer operations
const
const transferAB1: STM.STM<undefined, InsufficientFundsError | Error, never>
transferAB1
=
const transfer: (from: Account, to: Account, amount: number) => STM.STM<undefined, InsufficientFundsError | Error, never>
transfer
(
const accountA: Account
accountA
,
const accountB: Account
accountB
, 100)
const
const transferBA1: STM.STM<undefined, InsufficientFundsError | Error, never>
transferBA1
=
const transfer: (from: Account, to: Account, amount: number) => STM.STM<undefined, InsufficientFundsError | Error, never>
transfer
(
const accountB: Account
accountB
,
const accountA: Account
accountA
, 50)
const
const transferAB2: STM.STM<undefined, InsufficientFundsError | Error, never>
transferAB2
=
const transfer: (from: Account, to: Account, amount: number) => STM.STM<undefined, InsufficientFundsError | Error, never>
transfer
(
const accountA: Account
accountA
,
const accountB: Account
accountB
, 200)
const
const transferAB_Insufficient: STM.STM<undefined, InsufficientFundsError | Error, never>
transferAB_Insufficient
=
const transfer: (from: Account, to: Account, amount: number) => STM.STM<undefined, InsufficientFundsError | Error, never>
transfer
(
const accountA: Account
accountA
,
const accountB: Account
accountB
, 1000) // Will likely fail
// Create effects by committing the STM transactions
const
const effectAB1: Effect.Effect<undefined, InsufficientFundsError | Error, never>
effectAB1
=
import STM
STM
.
const commit: <undefined, InsufficientFundsError | Error, never>(self: STM.STM<undefined, InsufficientFundsError | Error, never>) => Effect.Effect<...>

Commits this transaction atomically.

@since2.0.0

commit
(
const transferAB1: STM.STM<undefined, InsufficientFundsError | Error, never>
transferAB1
).
Pipeable.pipe<Effect.Effect<undefined, InsufficientFundsError | Error, never>, Effect.Effect<undefined, InsufficientFundsError | Error, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const withLogSpan: (label: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R> (+1 overload)

Adds a log span to an effect for tracking and logging its execution duration.

Details

This function wraps an effect with a log span, providing performance monitoring and debugging capabilities. The log span tracks the duration of the wrapped effect and logs it with the specified label. This is particularly useful when analyzing time-sensitive operations or understanding the execution time of specific tasks in your application.

The logged output will include the label and the total time taken for the operation. The span information is included in the log metadata, making it easy to trace performance metrics in logs.

Example

import { Effect } from "effect"
const program = Effect.gen(function*() {
yield* Effect.sleep("1 second")
yield* Effect.log("The job is finished!")
}).pipe(Effect.withLogSpan("myspan"))
Effect.runFork(program)
// timestamp=... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms

@since2.0.0

withLogSpan
('Transfer_A->B_100'))
const
const effectBA1: Effect.Effect<undefined, InsufficientFundsError | Error, never>
effectBA1
=
import STM
STM
.
const commit: <undefined, InsufficientFundsError | Error, never>(self: STM.STM<undefined, InsufficientFundsError | Error, never>) => Effect.Effect<...>

Commits this transaction atomically.

@since2.0.0

commit
(
const transferBA1: STM.STM<undefined, InsufficientFundsError | Error, never>
transferBA1
).
Pipeable.pipe<Effect.Effect<undefined, InsufficientFundsError | Error, never>, Effect.Effect<undefined, InsufficientFundsError | Error, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const withLogSpan: (label: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R> (+1 overload)

Adds a log span to an effect for tracking and logging its execution duration.

Details

This function wraps an effect with a log span, providing performance monitoring and debugging capabilities. The log span tracks the duration of the wrapped effect and logs it with the specified label. This is particularly useful when analyzing time-sensitive operations or understanding the execution time of specific tasks in your application.

The logged output will include the label and the total time taken for the operation. The span information is included in the log metadata, making it easy to trace performance metrics in logs.

Example

import { Effect } from "effect"
const program = Effect.gen(function*() {
yield* Effect.sleep("1 second")
yield* Effect.log("The job is finished!")
}).pipe(Effect.withLogSpan("myspan"))
Effect.runFork(program)
// timestamp=... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms

@since2.0.0

withLogSpan
('Transfer_B->A_50'))
const
const effectAB2: Effect.Effect<undefined, InsufficientFundsError | Error, never>
effectAB2
=
import STM
STM
.
const commit: <undefined, InsufficientFundsError | Error, never>(self: STM.STM<undefined, InsufficientFundsError | Error, never>) => Effect.Effect<...>

Commits this transaction atomically.

@since2.0.0

commit
(
const transferAB2: STM.STM<undefined, InsufficientFundsError | Error, never>
transferAB2
).
Pipeable.pipe<Effect.Effect<undefined, InsufficientFundsError | Error, never>, Effect.Effect<undefined, InsufficientFundsError | Error, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const withLogSpan: (label: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R> (+1 overload)

Adds a log span to an effect for tracking and logging its execution duration.

Details

This function wraps an effect with a log span, providing performance monitoring and debugging capabilities. The log span tracks the duration of the wrapped effect and logs it with the specified label. This is particularly useful when analyzing time-sensitive operations or understanding the execution time of specific tasks in your application.

The logged output will include the label and the total time taken for the operation. The span information is included in the log metadata, making it easy to trace performance metrics in logs.

Example

import { Effect } from "effect"
const program = Effect.gen(function*() {
yield* Effect.sleep("1 second")
yield* Effect.log("The job is finished!")
}).pipe(Effect.withLogSpan("myspan"))
Effect.runFork(program)
// timestamp=... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms

@since2.0.0

withLogSpan
('Transfer_A->B_200'))
const
const effectAB_Insufficient: Effect.Effect<void | undefined, Error, never>
effectAB_Insufficient
=
import STM
STM
.
const commit: <undefined, InsufficientFundsError | Error, never>(self: STM.STM<undefined, InsufficientFundsError | Error, never>) => Effect.Effect<...>

Commits this transaction atomically.

@since2.0.0

commit
(
const transferAB_Insufficient: STM.STM<undefined, InsufficientFundsError | Error, never>
transferAB_Insufficient
).
Pipeable.pipe<Effect.Effect<undefined, InsufficientFundsError | Error, never>, Effect.Effect<void | undefined, Error, never>, Effect.Effect<...>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
// Catch the specific error
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const catchTag: <"InsufficientFundsError", InsufficientFundsError | Error, void, never, never>(k: "InsufficientFundsError", f: (e: InsufficientFundsError) => Effect.Effect<...>) => <A, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+1 overload)

Catches and handles specific errors by their _tag field, which is used as a discriminator.

When to Use

catchTag is useful when your errors are tagged with a readonly _tag field that identifies the error type. You can use this function to handle specific error types by matching the _tag value. This allows for precise error handling, ensuring that only specific errors are caught and handled.

The error type must have a readonly _tag field to use catchTag. This field is used to identify and match errors.

Example (Handling Errors by Tag)

import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, ValidationError, never>
// ▼
const recovered = program.pipe(
// Only handle HttpError errors
Effect.catchTag("HttpError", (_HttpError) =>
Effect.succeed("Recovering from HttpError")
)
)

@seecatchTags for a version that allows you to handle multiple error types at once.

@since2.0.0

catchTag
('InsufficientFundsError', (
error: InsufficientFundsError
error
) =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const logWarning: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

Logs messages at the WARNING log level.

Details

This function logs messages at the WARNING level, suitable for highlighting potential issues that are not errors but may require attention. These messages indicate that something unexpected occurred or might lead to errors in the future.

@since2.0.0

logWarning
(`Transfer failed: ${
error: InsufficientFundsError
error
.
message: string
message
}`,
error: InsufficientFundsError
error
)
),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const withLogSpan: (label: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R> (+1 overload)

Adds a log span to an effect for tracking and logging its execution duration.

Details

This function wraps an effect with a log span, providing performance monitoring and debugging capabilities. The log span tracks the duration of the wrapped effect and logs it with the specified label. This is particularly useful when analyzing time-sensitive operations or understanding the execution time of specific tasks in your application.

The logged output will include the label and the total time taken for the operation. The span information is included in the log metadata, making it easy to trace performance metrics in logs.

Example

import { Effect } from "effect"
const program = Effect.gen(function*() {
yield* Effect.sleep("1 second")
yield* Effect.log("The job is finished!")
}).pipe(Effect.withLogSpan("myspan"))
Effect.runFork(program)
// timestamp=... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms

@since2.0.0

withLogSpan
('Transfer_A->B_1000')
)
// Run transfers concurrently
const
const fibers: Fiber.Fiber<void[], InsufficientFundsError | Error>
fibers
= yield*
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const forkAll: <Effect.Effect<undefined, InsufficientFundsError | Error, never> | Effect.Effect<void | undefined, Error, never>>(effects: Iterable<...>, options?: {
readonly discard?: false | undefined;
} | undefined) => Effect.Effect<...> (+3 overloads)

Returns an effect that forks all of the specified values, and returns a composite fiber that produces a list of their results, in order.

@since2.0.0

forkAll
([
const effectAB1: Effect.Effect<undefined, InsufficientFundsError | Error, never>
effectAB1
,
const effectBA1: Effect.Effect<undefined, InsufficientFundsError | Error, never>
effectBA1
,
const effectAB2: Effect.Effect<undefined, InsufficientFundsError | Error, never>
effectAB2
,
const effectAB_Insufficient: Effect.Effect<void | undefined, Error, never>
effectAB_Insufficient
])
// Wait for all transfers to complete or fail
yield*
import Fiber
Fiber
.
const join: <void[], InsufficientFundsError | Error>(self: Fiber.Fiber<void[], InsufficientFundsError | Error>) => Effect.Effect<...>

Joins the fiber, which suspends the joining fiber until the result of the fiber has been determined. Attempting to join a fiber that has erred will result in a catchable error. Joining an interrupted fiber will result in an "inner interruption" of this fiber, unlike interruption triggered by another fiber, "inner interruption" can be caught and recovered.

@since2.0.0

join
(
const fibers: Fiber.Fiber<void[], InsufficientFundsError | Error>
fibers
)
// Check final balances (atomically reads both in one transaction)
const
const finalBalances: [number, number]
finalBalances
= yield*
import STM
STM
.
const commit: <[number, number], never, never>(self: STM.STM<[number, number], never, never>) => Effect.Effect<[number, number], never, never>

Commits this transaction atomically.

@since2.0.0

commit
(
import STM
STM
.
const all: STM.All.Signature
<[STM.STM<number, never, never>, STM.STM<number, never, never>], NoExcessProperties<STM.All.Options, unknown>>(arg: [STM.STM<number, never, never>, STM.STM<...>], options?: NoExcessProperties<...> | undefined) => STM.STM<...>

Runs all the provided transactional effects in sequence respecting the structure provided in input.

Supports multiple arguments, a single argument tuple / array or record / struct.

@since2.0.0

all
([
import TRef
TRef
.
const get: <number>(self: TRef.TRef<number>) => STM.STM<number, never, never>

@since2.0.0

get
(
const accountA: Account
accountA
.
Account.balance: TRef.TRef<number>
balance
),
import TRef
TRef
.
const get: <number>(self: TRef.TRef<number>) => STM.STM<number, never, never>

@since2.0.0

get
(
const accountB: Account
accountB
.
Account.balance: TRef.TRef<number>
balance
)])
)
yield*
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const logInfo: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

Logs messages at the INFO log level.

Details

This function logs messages at the INFO level, suitable for general application events or operational messages. INFO logs are shown by default and are commonly used for highlighting normal, non-error operations.

@since2.0.0

logInfo
(`Final Balances - A: ${
const finalBalances: [number, number]
finalBalances
[0]}, B: ${
const finalBalances: [number, number]
finalBalances
[1]}`)
yield*
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const logInfo: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

Logs messages at the INFO log level.

Details

This function logs messages at the INFO level, suitable for general application events or operational messages. INFO logs are shown by default and are commonly used for highlighting normal, non-error operations.

@since2.0.0

logInfo
(`Total balance: ${
const finalBalances: [number, number]
finalBalances
[0] +
const finalBalances: [number, number]
finalBalances
[1]}`) // Should remain constant (1500)
}).
Pipeable.pipe<Effect.Effect<void, InsufficientFundsError | Error, never>, Effect.Effect<void, InsufficientFundsError | Error, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Logger
Logger
.
const withMinimumLogLevel: (level: LogLevel.LogLevel) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R> (+1 overload)

Sets the minimum log level for subsequent logging operations, allowing control over which log messages are displayed based on their severity.

@example

import { Effect, Logger, LogLevel } from "effect"
const program = Effect.logDebug("message1").pipe(Logger.withMinimumLogLevel(LogLevel.Debug))
Effect.runFork(program)
// timestamp=... level=DEBUG fiber=#0 message=message1

@since2.0.0

withMinimumLogLevel
(
import LogLevel
LogLevel
.
const Info: LogLevel.LogLevel

@since2.0.0

@since2.0.0

Info
))
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const runPromise: <void, InsufficientFundsError | Error>(effect: Effect.Effect<void, InsufficientFundsError | Error, never>, options?: {
readonly signal?: AbortSignal;
} | undefined) => Promise<...>

Executes an effect and returns the result as a Promise.

Details

This function runs an effect and converts its result into a Promise. If the effect succeeds, the Promise will resolve with the successful result. If the effect fails, the Promise will reject with an error, which includes the failure details of the effect.

The optional options parameter allows you to pass an AbortSignal for cancellation, enabling more fine-grained control over asynchronous tasks.

When to Use

Use this function when you need to execute an effect and work with its result in a promise-based system, such as when integrating with third-party libraries that expect Promise results.

Example (Running a Successful Effect as a Promise)

import { Effect } from "effect"
Effect.runPromise(Effect.succeed(1)).then(console.log)
// Output: 1

Example (Handling a Failing Effect as a Rejected Promise)

import { Effect } from "effect"
Effect.runPromise(Effect.fail("my error")).catch(console.error)
// Output:
// (FiberFailure) Error: my error

@seerunPromiseExit for a version that returns an Exit type instead of rejecting.

@since2.0.0

runPromise
(
const program: Effect.Effect<void, InsufficientFundsError | Error, never>
program
)

When you run this, you’ll see the transfer logs interleaved, but STM guarantees that each successful transfer updates both account balances atomically. The total balance across both accounts will remain constant (1500 in this case), demonstrating consistency. The transfer attempting to take 1000 from A will fail gracefully with the InsufficientFundsError.

Sometimes, a transaction should only proceed if a certain condition is met, and if not, it should wait and automatically retry later when the state might have changed. This is where STM.check comes in.

STM.check takes a boolean condition. If the condition is true, the transaction continues. If it’s false, the transaction suspends and automatically retries later when any TRef read within that transaction is modified by another committing transaction.

Example: Waiting for sufficient funds before proceeding.

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
import STM
STM
,
import TRef
TRef
} from "effect";
4 collapsed lines
interface
interface Account
Account
{
readonly
Account.id: string
id
: string;
readonly
Account.balance: TRef.TRef<number>
balance
:
import TRef
TRef
.
interface TRef<in out A>

A TRef<A> is a purely functional description of a mutable reference that can be modified as part of a transactional effect. The fundamental operations of a TRef are set and get. set transactionally sets the reference to a new value. get gets the current value of the reference.

NOTE: While TRef<A> provides the transactional equivalent of a mutable reference, the value inside the TRef should be immutable.

@since2.0.0

@since2.0.0

TRef
<number>;
}
// Wait until the balance is at least 'amount'
const
const waitForBalance: (account: Account, amount: number) => STM.STM<void, never, never>
waitForBalance
= (
account: Account
account
:
interface Account
Account
,
amount: number
amount
: number
):
import STM
STM
.
interface STM<out A, out E = never, out R = never>

STM<A, E, R> represents an effect that can be performed transactionally, resulting in a failure E or a value A that may require an environment R to execute.

Software Transactional Memory is a technique which allows composition of arbitrary atomic operations. It is the software analog of transactions in database systems.

The API is lifted directly from the Haskell package Control.Concurrent.STM although the implementation does not resemble the Haskell one at all.

See http://hackage.haskell.org/package/stm-2.5.0.0/docs/Control-Concurrent-STM.html

STM in Haskell was introduced in:

Composable memory transactions, by Tim Harris, Simon Marlow, Simon Peyton Jones, and Maurice Herlihy, in ACM Conference on Principles and Practice of Parallel Programming 2005.

See https://www.microsoft.com/en-us/research/publication/composable-memory-transactions/

See also: Lock Free Data Structures using STMs in Haskell, by Anthony Discolo, Tim Harris, Simon Marlow, Simon Peyton Jones, Satnam Singh) FLOPS 2006: Eighth International Symposium on Functional and Logic Programming, Fuji Susono, JAPAN, April 2006

https://www.microsoft.com/en-us/research/publication/lock-free-data-structures-using-stms-in-haskell/

The implemtation is based on the ZIO STM module, while JS environments have no race conditions from multiple threads STM provides greater benefits for synchronization of Fibers and transactional data-types can be quite useful.

@since2.0.0

@since2.0.0

STM
<void, never, never> =>
import STM
STM
.
const gen: <unknown, YieldWrap<STM.STM<void, never, never>>, void>(...args: [self: unknown, body: (this: unknown, resume: STM.Adapter) => Generator<YieldWrap<STM.STM<void, never, never>>, void, never>] | [body: ...]) => STM.STM<...>

@since2.0.0

gen
(function* () {
const
const balance: number
balance
= yield*
import TRef
TRef
.
const get: <number>(self: TRef.TRef<number>) => STM.STM<number, never, never>

@since2.0.0

get
(
account: Account
account
.
Account.balance: TRef.TRef<number>
balance
)
// If balance < amount, STM.check(false) causes the transaction to retry later
yield*
import STM
STM
.
const check: (predicate: LazyArg<boolean>) => STM.STM<void>

Checks the condition, and if it's true, returns unit, otherwise, retries.

@since2.0.0

check
(() =>
const balance: number
balance
>=
amount: number
amount
)
// If we reach here, the condition was true
})
// A transfer that waits if funds are insufficient, instead of failing
const
const transferOrWait: (from: Account, to: Account, amount: number) => STM.STM<void, Error, never>
transferOrWait
= (
from: Account
from
:
interface Account
Account
,
to: Account
to
:
interface Account
Account
,
amount: number
amount
: number
):
import STM
STM
.
interface STM<out A, out E = never, out R = never>

STM<A, E, R> represents an effect that can be performed transactionally, resulting in a failure E or a value A that may require an environment R to execute.

Software Transactional Memory is a technique which allows composition of arbitrary atomic operations. It is the software analog of transactions in database systems.

The API is lifted directly from the Haskell package Control.Concurrent.STM although the implementation does not resemble the Haskell one at all.

See http://hackage.haskell.org/package/stm-2.5.0.0/docs/Control-Concurrent-STM.html

STM in Haskell was introduced in:

Composable memory transactions, by Tim Harris, Simon Marlow, Simon Peyton Jones, and Maurice Herlihy, in ACM Conference on Principles and Practice of Parallel Programming 2005.

See https://www.microsoft.com/en-us/research/publication/composable-memory-transactions/

See also: Lock Free Data Structures using STMs in Haskell, by Anthony Discolo, Tim Harris, Simon Marlow, Simon Peyton Jones, Satnam Singh) FLOPS 2006: Eighth International Symposium on Functional and Logic Programming, Fuji Susono, JAPAN, April 2006

https://www.microsoft.com/en-us/research/publication/lock-free-data-structures-using-stms-in-haskell/

The implemtation is based on the ZIO STM module, while JS environments have no race conditions from multiple threads STM provides greater benefits for synchronization of Fibers and transactional data-types can be quite useful.

@since2.0.0

@since2.0.0

STM
<void,
interface Error
Error
, never> =>
import STM
STM
.
const gen: <unknown, YieldWrap<STM.STM<void, never, never>>, undefined>(...args: [self: unknown, body: (this: unknown, resume: STM.Adapter) => Generator<YieldWrap<STM.STM<void, never, never>>, undefined, never>] | [body: ...]) => STM.STM<...>

@since2.0.0

gen
(function* (
_: STM.Adapter
_
) {
// Ensure amount is positive
if (
amount: number
amount
<= 0) {
return yield*
import STM
STM
.
const die: (defect: unknown) => STM.STM<never>

Fails the transactional effect with the specified defect.

@since2.0.0

die
(new
var Error: ErrorConstructor
new (message?: string) => Error
Error
('Transfer amount must be positive'))
}
// Wait until 'from' account has enough balance
yield*
const waitForBalance: (account: Account, amount: number) => STM.STM<void, never, never>
waitForBalance
(
from: Account
from
,
amount: number
amount
)
// Now we know funds are sufficient (or were when check passed)
// STM guarantees atomicity, so we still read/write safely
const
const fromBalance: number
fromBalance
= yield*
import TRef
TRef
.
const get: <number>(self: TRef.TRef<number>) => STM.STM<number, never, never>

@since2.0.0

get
(
from: Account
from
.
Account.balance: TRef.TRef<number>
balance
)
// Double-check (optional but safe): STM may retry if 'from.balance' changed
// between the check and here, but the check *will* be re-evaluated.
// yield* _(STM.check(fromBalance >= amount)) // Usually redundant if check was just performed
yield*
import TRef
TRef
.
const set: <number>(self: TRef.TRef<number>, value: number) => STM.STM<void> (+1 overload)

@since2.0.0

set
(
from: Account
from
.
Account.balance: TRef.TRef<number>
balance
,
const fromBalance: number
fromBalance
-
amount: number
amount
)
yield*
import TRef
TRef
.
const update: <number>(self: TRef.TRef<number>, f: (a: number) => number) => STM.STM<void> (+1 overload)

@since2.0.0

update
(
to: Account
to
.
Account.balance: TRef.TRef<number>
balance
, (
b: number
b
) =>
b: number
b
+
amount: number
amount
)
})
// You would then STM.commit(transferOrWait(...))

Effect’s STM provides a robust, composable, and high-level abstraction for managing shared mutable state in concurrent programs. By using TRef to hold state and STM to describe atomic transactions, you can avoid the complexities and pitfalls of manual locking while benefiting from:

  • Automatic Atomicity: Ensures transactions complete fully or not at all.
  • Consistency: Keeps your shared state valid.
  • Isolation: Prevents interference between concurrent transactions.
  • Composability: Allows building complex transactions from simpler ones.
  • Integration with Effect: Leverages Effect’s powerful concurrency, error handling, and resource management features.

STM is an invaluable tool when you need to coordinate concurrent access to shared data safely and effectively in your Effect applications.