Choosing Between PureScript's Aff and Effect Monads for Asynchronous Code
A decision guide for choosing PureScript's Aff monad versus Effect monad for asynchronous programming, with a comparison table, trade‑off explanation, and concrete code examples.
07 Sept 2025, 23:31 UTC

Decision and Constraints
When writing PureScript code that interacts with a JavaScript environment (browser or Node), you must decide whether to model asynchronous operations with the Aff monad or keep them in the simpler Effect monad. The choice hinges on whether you need built‑in support for composing async actions, parallel execution, and error propagation.
Constraints
- Target platform: JavaScript (browser or Node).
- PureScript version: 0.15.x (where
Afflives inEffect.Aff). - Dependencies:
Affrequires thepurescript-affpackage (and its transitive depspurescript-either,purescript-transformers).Effectis provided by the corepurescript-effectpackage and needs no extra installation.
Comparison Table
| Aspect | Aff | Effect |
|---|---|---|
| Async support | Native (forkAff, awaitAff, Affjax) | Requires manual callbacks or JS FFI |
| Composition | Monadic bind, parallel via Aff.parallel | Sequential only (do‑notation) |
| Error handling | Either‑like semantics (EitherT, tryEither) | Manual propagation |
| Runtime overhead | Minimal (few closures) | Zero extra cost |
| Learning curve | Moderate (understand transformers) | Low (direct effect) |
| JS interop | Easy via Aff.fromPromise, Affjax | Direct foreign imports |
Trade‑offs Explanation
Aff gives you a monad that models asynchronous, potentially failing computations. Its Bind instance lets you chain actions sequentially, while Aff.parallel lets you run independent async effects concurrently. Errors are captured in an Either-like structure, so a single tryEither can convert a computation to Either Error a.
The downside is a small runtime cost due to the underlying free‑monad implementation and a modest increase in cognitive load because you must understand concepts like EitherT and Affjax. For purely synchronous side‑effects—such as mutating the DOM, writing to console.log, or reading a synchronous config—Effect is sufficient and incurs no extra overhead.
Concrete Implementation
Using Aff for an AJAX request
module Main where
import Effect (Effect)
import Effect.Aff (Aff, launchAff_, mapEffect)
import Effect.Class.Console (logError, logShow)
import Effect.Ajax (ajax, AjaxError)
import Data.Either (Either(..))
main :: Effect Unit
main = launchAff_ $ do
result <- ajax { url: "/api/data", method: "GET", responseType: "json" }
case result of
Left err -> logError $ "Request failed: " <> show err
Right resp -> logShow resp
This snippet shows the typical workflow: launchAff_ starts the computation in the JavaScript event loop, ajax returns an Aff (Either AjaxError a), and the do‑notation binds the result while preserving error information.
Using Effect for a simple console log
module Main where
import Effect (Effect)
import Effect.Class.Console (log)
main :: Effect Unit
main = log "Hello from Effect"
Here no asynchronous behavior is needed; the Effect monad directly performs the side‑effect.
Verification Steps
- Create a minimal project:
spago init. - Add dependencies:
spago install purescript-aff purescript-dom purescript-ajax(for the Aff example) and ensurepurescript-effectis present. - Place the Aff example in
src/Main.purs, compile withspago build, and open the generatedindex.jsin a browser. Verify that a network request is made and the response appears in the console. - Replace
src/Main.purswith the Effect example, rebuild, and run. Confirm that the string "Hello from Effect" is printed without any async machinery. - Optionally, write a unit test using
purescript-affjaxandspago testto assert that anAffcomputation yields aRightvalue on success.
Limitations and Practical Checks
Affcomputations are cold: they do nothing until launched withlaunchAff_(or similar). Forgetting this results in a silent no‑op.- When mixing
Affwith raw JavaScript promises, convert viaAff.fromPromiseorAff.tryEitherto avoid unhandled rejections. - Parallel combinators assume commutative effects; using them with non‑commutative DOM writes can cause race conditions.
- To check that an
Affcomputation actually ran, attach a side‑effect (e.g., a console log) inside thedoblock and verify its output.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.