Skip to content

Build1 publisher2 min readPublished

Effect 4 rebuilds STM inside Effect around an implicit transaction boundary

Effect 4 moves STM into Effect as TxRef and Effect.tx, and a transfer ported without the Effect.tx wrapper runs as four separate transactions. Effect 3 teams porting TRef code must place each boundary by hand, since the types barely track it.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying Effect 4 rebuilds STM inside Effect around an implicit transaction boundary
Generated illustration

What happened

  • In v4 the transaction journal lives in a service, Effect.Transaction, and Effect.tx sets the boundary the way Effect.scoped provides a Scope.
  • Most of the port is renaming: TRef becomes TxRef, STM.retry becomes Effect.txRetry, and the v4 example drops STM.commit and yields the transaction directly.
  • The post's author calls v3's ban on running Effect code inside STM the model's biggest limitation, since it makes logging and debugging harder.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Every STM.gen block in a v3 codebase needs a deliberate Effect.tx placed around its Effect.gen; renaming TRef to TxRef alone changes how many transactions run.
  • constraint Because Transaction rarely shows in the R channel, reviewers and the compiler get little help spotting a missing or misplaced boundary during a port.
  • exposure Single-fiber tests will pass a port with a missing Effect.tx, so the lost atomicity first shows up in production code where fibers contend for the same refs.

Effect 3 gets atomicity from a separate type, according to a walkthrough of the change posted on dev.to. STM is synchronous and pure, and unlike an Effect it cannot be interrupted partway through [1]. While a block runs, it journals every TRef it touches: the ref, the value it held when the transaction began, and its current value inside the transaction [3]. STM.commit compares those starting values with the live ones. If they match, the writes apply. If any changed, the block restarts, and this repeats until a commit succeeds [4]. "This is possible precisely because STM is a pure computation," the post's author wrote [5].

The v4 port has one structural change. STM.gen becomes two calls, an Effect.tx wrapped around an ordinary Effect.gen [17]. That wrapper decides how many transactions run. Tx* operations are themselves wrapped in Effect.tx, so a bare TxRef.set is a tiny transaction with its own journal [9]. When Effect.tx finds an Effect.Transaction already in the surrounding Context, it attaches to that transaction and reuses it [10]. Inside an outer Effect.tx, the transfer's two reads and two writes all land in one journal. Without it, each commits on its own [11].

The author's objection is about visibility. Transaction is implicit and "barely appears in the R channel," and the author says that makes boundaries harder to track [12]. "I think it could have been made explicit, similar to how Scope works," the author wrote [13]. I agree, and the reason is specific to porting. In v3 a transaction had its own type and reached the program through STM.commit [1][2]. In v4, a port that renames TRef, deletes STM.commit and forgets Effect.tx still runs, with no boundary around the transfer [11].

The other v4 change is about duration. Effect code, including asynchronous code, can now run inside a transaction [14]. The author warns that this lets a transaction stretch over time, giving the refs it read longer to change and raising the chance of conflicts [15]. The author says the basic idea is unchanged from v3 [16], and in v3 a conflict means the block starts again [4]. A transaction that waits on asynchronous work while other fibers write the same refs will restart more often than the short pure block it replaced [19].

What to watch

  • Whether Effect 4 re-runs side effects inside a transaction, such as log lines, each time a conflict forces a restart.
  • Whether Effect 3's STM and TRef modules still ship in v4; if they do, teams can port one module at a time.
  • Whether Effect's maintainers make Transaction explicit in the R channel, as the post's author proposed, so the compiler can flag a missing Effect.tx.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories