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.
The Core Building Blocks: TRef and STM
STM in Effect revolves around two main types:
TRef<A>: Transactional References
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.
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)
Attaches callbacks for the resolution and/or rejection of the Promise.
@param ― onfulfilled The callback to execute when the Promise is resolved.
@param ― onrejected The callback to execute when the Promise is rejected.
@returns ― A Promise for the completion of which ever callback is executed.
then((
counterRef: TRef.TRef<number>
counterRef) => {
7
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(newError('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
constname='Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console class:
constout=getStreamSomehow();
consterr=getStreamSomehow();
constmyConsole=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(newError('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
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()).
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:
// For demonstration, we'll pretend we have a counter
6
declareconst
constcounter:TRef.TRef<number>
counter:
import TRef
TRef.
interfaceTRef<inoutA>
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.
Notice that these operations describe what should happen in a transaction, but they don’t execute it immediately.
Committing Transactions: Running STM
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>.
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.
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.
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.
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)
@see ― runPromiseExit for a version that returns an Exit type instead
of rejecting.
@since ― 2.0.0
runPromise(
constprogram: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.
Example: Atomic Bank Transfers
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.
// Represents a bank account with a balance stored in a TRef
13
interface
interfaceAccount
Account {
14
readonly
Account.id: string
id:string;
15
readonly
Account.balance: TRef.TRef<number>
balance:
import TRef
TRef.
interfaceTRef<inoutA>
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.
@since ― 2.0.0
@since ― 2.0.0
TRef<number>;
16
}
17
18
/**
19
* Describes an atomic transfer between two accounts.
20
* Fails with InsufficientFundsError if the 'from' account lacks funds.
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.
Running Concurrent Transfers
Now, let’s simulate multiple concurrent transfers using Effect’s concurrency features:
1
import {
import Effect
@since ― 2.0.0
@since ― 2.0.0
@since ― 2.0.0
Effect,
import Data
Data,
import STM
STM,
import TRef
TRef,
import Fiber
Fiber,
import Logger
Logger,
import LogLevel
LogLevel } from"effect";
2
43 collapsed lines
3
interface
interfaceAccount
Account {
4
readonly
Account.id: string
id:string;
5
readonly
Account.balance: TRef.TRef<number>
balance:
import TRef
TRef.
interfaceTRef<inoutA>
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.
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.
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.
constwithLogSpan: (label:string) => <A, E, R>(effect:Effect.Effect<A, E, R>) =>Effect.Effect<A, E, R> (+1overload)
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"
constprogram= 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
constwithLogSpan: (label:string) => <A, E, R>(effect:Effect.Effect<A, E, R>) =>Effect.Effect<A, E, R> (+1overload)
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"
constprogram= 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
constwithLogSpan: (label:string) => <A, E, R>(effect:Effect.Effect<A, E, R>) =>Effect.Effect<A, E, R> (+1overload)
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"
constprogram= 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
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.
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.
@since ― 2.0.0
logWarning(`Transfer failed: ${
error: InsufficientFundsError
error.
message: string
message}`,
error: InsufficientFundsError
error)
74
),
75
import Effect
@since ― 2.0.0
@since ― 2.0.0
@since ― 2.0.0
Effect.
constwithLogSpan: (label:string) => <A, E, R>(effect:Effect.Effect<A, E, R>) =>Effect.Effect<A, E, R> (+1overload)
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"
constprogram= 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
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.
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.
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.
@since ― 2.0.0
logInfo(`Total balance: ${
constfinalBalances: [number, number]
finalBalances[0] +
constfinalBalances: [number, number]
finalBalances[1]}`) // Should remain constant (1500)
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)
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.
Conditional Waiting: STM.check and STM.retry
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.
1
import {
import Effect
@since ― 2.0.0
@since ― 2.0.0
@since ― 2.0.0
Effect,
import STM
STM,
import TRef
TRef } from"effect";
2
4 collapsed lines
3
interface
interfaceAccount
Account {
4
readonly
Account.id: string
id:string;
5
readonly
Account.balance: TRef.TRef<number>
balance:
import TRef
TRef.
interfaceTRef<inoutA>
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.
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.
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 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
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.
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.
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 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
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.
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.