Choosing a PureScript FFI Boundary for JavaScript Calls
Decide which PureScript FFI style to use when calling JavaScript, with a comparison table, trade-offs, and a validated spago workflow.
24 Jan 2026, 04:09 UTC

The decision
You have a JavaScript function or library and you need to call it from PureScript. The PureScript compiler cannot see inside JavaScript, so every call crosses an unchecked boundary unless you declare it explicitly. The decision is not whether to use FFI, but which FFI declaration style matches the JavaScript value you are importing and whether that call performs side effects or asynchronous work.
Use this guide when you are adding a new JavaScript dependency to a PureScript project and want to keep the boundary typed, maintainable, and easy to validate.
Supported FFI options
| Option | Best for | Type safety | Effect tracking |
|---|---|---|---|
foreign import with explicit PureScript types | JavaScript functions with known argument and return shapes | Compile-time checked against the declaration | Add Effect or Aff to the return type |
data foreign type (opaque handle) | JavaScript objects such as DOM nodes or library instances | Prevents accidental misuse, but hides the representation | Pair with Effect for impure operations |
newtype Foreign wrapper | Values with no known PureScript representation | Low; it is an escape hatch | None by itself |
Raw Foreign type | Quick prototypes only | None; defeats parametricity | None by itself |
Foreign is the built-in type for an unchecked JavaScript value. A newtype is a zero-cost wrapper that creates a distinct PureScript type without adding runtime representation. Effect marks a computation that performs side effects, while Aff tracks asynchronous side effects.
Trade-offs
Typed foreign import catches signature mistakes at compile time, but the compiler cannot verify that the JavaScript export actually exists or matches the declared arity. A missing export or wrong argument count fails at runtime.
Opaque data foreign types protect you from treating a JavaScript object as a primitive, but you cannot inspect or transform the value inside PureScript without more FFI. newtype Foreign is easy to write, but it discards type information and can let representation errors reach runtime.
Never wrap an effectful import in a pure type and extract it with unsafePerformEffect. That breaks referential transparency and makes effect ordering unpredictable. Modern PureScript uses Effect and Aff; the older Eff effect row system is obsolete for new FFI code.
Concrete implementation
Assume a PureScript project managed with spago. You want to call a small JavaScript addition function.
PureScript module
module Main where
import Effect (Effect)
import Effect.Console (log)
foreign import add :: Int -> Int -> Effect Int
main :: Effect Unit
main = do
result <- add 1 2
log ("Result: " <> show result)
JavaScript module
Place this file where your PureScript FFI resolution expects it (commonly alongside the PureScript module or in the project's FFI directory, depending on your build configuration).
exports.add = function (a, b) {
return a + b;
};
The Effect Int return type tells PureScript that add may perform side effects. Because addition is pure in practice, you could declare it without Effect, but keeping Effect is safer if the JavaScript implementation later changes.
Validation
- Run
spago buildfrom the project root. This compiles the PureScript modules and type-checks the FFI declarations. No special permissions are required beyond normal file access. - Run
spago testif you have a test suite that importsMainand asserts the returned value. This requires test dependencies to be installed. - Run the compiled JavaScript in Node to confirm the export is invoked:
node output/Main.js(path varies by build output configuration). Expect the program to printResult: 3and exit without a representation error.
If spago build succeeds but Node throws a runtime error such as add is not a function, the mismatch is in the JavaScript export or module path, not the PureScript type declaration. FFI types are not verified against the JavaScript implementation.
Limitations
- JSON-like values crossing the boundary can lose type information.
undefined,null, andNaNhave no direct PureScript equivalents and should be validated at the edge. - Changing a JavaScript export's arity does not produce a PureScript compile error; it produces a runtime failure.
- This example assumes a modern PureScript toolchain with
EffectandAff. Older projects usingEffneed migration before applying this pattern.
Practical check
After changing a JavaScript export, rerun spago build and then execute the compiled entry point in Node. If the output matches the expected PureScript assertion, the boundary is intact. If not, inspect the emitted JavaScript or the Node stack trace to locate the representation mismatch before adding more FFI imports.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.