Choosing Between PureScript Effect and Aff for Side Effects
A decision guide for PureScript developers: use Effect for immediate synchronous side‑effects and Aff for cancellable asynchronous work. Includes constraints, a comparison table, trade‑offs, and a runnable code example with build steps.
27 Sept 2025, 18:23 UTC

Decision: When to Use Effect vs Aff
PureScript separates synchronous side‑effects from asynchronous ones. Use Effect for pure, synchronous operations such as logging, random number generation, or mutable state updates that complete immediately. Use Aff when the operation may pause—network requests, timers, file I/O, or any work that should not block the JavaScript event loop.
Constraints
Effectis in the core library; no extra dependency required.Afflives in thepurescript-affpackage and needs a runtime that provides async primitives (Node.js, browsers withsetTimeout/Promise).- Both are monads and can be stacked with transformers, but
Affadds cancellation and concurrency primitives.
Comparison Table
| Feature | Effect | Aff |
|---|---|---|
| Synchronous only | ✔ | ✖ |
| Asynchronous support | ✖ | ✔ |
| Cancellation | ✖ | ✔ |
| Monad transformer composition | ✔ | ✔ |
| Boilerplate | minimal | requires Aff.runAff |
Trade‑offs
Effect keeps the call stack simple and runs immediately. It is ideal for deterministic, fast side‑effects. The downside is that any blocking I/O will freeze the thread, which is unacceptable in single‑threaded JavaScript environments.
Aff introduces an asynchronous boundary. The extra runAff call schedules the computation on the event loop, enabling non‑blocking I/O and cancellation via Aff.cancel. The cost is a slightly more verbose setup and the need to reason about callback timing in tests.
Concrete Example
The following module demonstrates a synchronous log with Effect and a delayed HTTP‑like request with Aff.
module Main where
import Prelude
import Effect (Effect)
import Effect.Console (log)
import Aff (Aff, runAff, delay, makeAff)
import Aff.Class (liftEffect)
-- Synchronous side‑effect
syncLog :: Effect Unit
syncLog = log "Sync log executed immediately"
-- Asynchronous side‑effect that resolves after 500 ms
asyncFetch :: Aff String
asyncFetch = do
liftEffect (log "Starting async fetch")
_ <- delay 500.0
liftEffect (log "Async fetch completed")
pure "{ data: 42 }"
main :: Effect Unit
main = do
syncLog
-- Run the Aff; the callback executes after the current stack
runAff (const (pure unit)) (const (pure unit)) asyncFetch
log "Main continues without waiting for asyncFetch"
Adding the Aff Dependency
Run the following in the project root (where spago.dhall lives). You need write permission to the project directory.
spago install purescript-aff
This updates spago.dhall and fetches the package. If the package is missing, the compiler will emit a “module Aff not found” error.
Building and Running
- Compile the project:
Expected result: exit code 0 and generated JavaScript underspago buildoutput/. - Execute the entry point with Node (requires Node ≥ 14):
You should see the synchronous log, the “Main continues…” line, then after ~500 ms the async fetch logs.node output/Main/index.js
Verification Checklist
spago buildsucceeds without missing‑module errors.- Running the compiled script prints the synchronous messages first, then the delayed messages, confirming non‑blocking behavior.
- In a test suite, call
Aff.runAffwith a delayedAff.delayand assert the callback fires after the expected timeout.
Limitations
Effectcannot perform true asynchronous I/O; using it for network calls will block the event loop.Aff.runAffis itself asynchronous; side‑effects inside the callback run after the current call stack, which can affect test ordering if not awaited properly.- Cancellation only works for
Afffibers created viaAff.forkorAff.parallel; plainEffectactions cannot be cancelled.
Diagram Labels
- Effect – synchronous
- Aff – asynchronous
- Cancellation support
- runAff boundary
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.