Bridging PureScript and JavaScript Safely: A Practical Guide to the FFI
PureScript’s Foreign Function Interface lets you call JavaScript from pure code. This post walks through declaring imports, writing the JS side, bundling, and testing the boundary—plus the trade‑offs you should know.
05 Mar 2026, 12:08 UTC

Problem: Calling JavaScript from PureScript
PureScript is a strongly typed, pure functional language that compiles to JavaScript. When you need to use an existing npm package, access browser APIs, or perform I/O, you must bridge to JavaScript. The Foreign Function Interface (FFI) is the official way to declare JavaScript functions and types so the compiler can type‑check calls while delegating the actual execution to the runtime.
Why the FFI Matters
PureScript’s type system guarantees that pure functions have no side‑effects. The FFI allows you to explicitly mark impure calls using the Eff effect type, keeping the compiler aware of potential side‑effects. Without this, you would either lose type safety or have to write unsafe wrappers that bypass the compiler’s checks.
Declaring a Foreign Import
The typical workflow starts with a small PureScript module that declares the JavaScript API you want to use. For example, to expose the global Math.sqrt function:
module Math.FFI where
import Prelude
import Effect (Effect)
foreign import sqrt :: Number -> Number
Here:
foreign importtells the compiler that the implementation lives outside PureScript.- The type signature
Number -> Numberis checked at compile time for arity and basic types. - Because
sqrtis pure, we omitEff; if the function performed I/O, we would writesqrt :: Number -> Effect Number.
Writing the JavaScript Side
FFI expects a separate JavaScript file that exports the symbols referenced in the PureScript module. Place it next to the module, e.g., src/js/Math.js:
// src/js/Math.js
exports.sqrt = Math.sqrt;
Key points:
- Use
exports.<name> = <value>to expose the function under the same name as the PureScript declaration. - The file must be located in a directory that Spago includes in the
spago.dhallsource path (defaultsrc). - Do not rename the function; the compiler will generate a direct call to
exports.sqrtin the compiled JavaScript.
Bundling with Spago
Once the declarations and implementations are in place, bundle the project for the target environment. For a Node or browser build, run:
# Create a new Spago project if you haven’t already
spago init
# Add the Math module and JS file
mkdir -p src/Math
mv Math.FFI.purs src/Math/
mkdir -p src/js
mv Math.js src/js/
# Build the bundle
spago build --output main.js
After the build, inspect main.js to confirm the foreign call appears as a direct reference to exports.sqrt:
// ... generated code
var sqrt = function (n) {
return exports.sqrt(n);
};
Running the bundle in Node:
node main.js
should yield the same result as calling Math.sqrt directly.
Testing the Boundary
Because the compiler does not verify the JavaScript implementation, you must test the boundary manually or with automated tests. A minimal sanity check is to compare the PureScript wrapper result with the direct JavaScript call:
main :: Effect Unit
main = do
let js = Math.sqrt 9.0
let ps = sqrt 9.0
logShow (js == ps)
Run the compiled bundle and verify the log prints true. If the result differs, the JS implementation or the foreign declaration is mismatched.
Trade‑Offs and Limitations
- Purity vs. Flexibility: The FFI bypasses PureScript’s purity guarantees. Any bug in the JavaScript side can corrupt state without compiler warnings.
- Manual Synchronization: If the JavaScript API changes (e.g., a parameter is added), you must update the PureScript declaration manually. The compiler will catch arity mismatches during build, but a missing export will cause a runtime error.
- Tooling Support: IDEs can provide type hints for the PureScript side but cannot introspect the JavaScript implementation. Documentation of the external API is essential.
- Effect Tracking: Using
Effor row‑polymorphic effect types allows the compiler to enforce that impure calls are only made within an effectful context, but it cannot guarantee that the JavaScript side behaves as declared.
Takeaway
PureScript’s FFI provides a type‑safe bridge to JavaScript, letting you leverage existing libraries while keeping the compiler’s guarantees intact. The key to safe interop is:
- Write precise type signatures and use
Efffor side‑effects. - Keep the JavaScript implementation in sync with the PureScript declarations.
- Test the boundary thoroughly to catch mismatches early.
- Document the external API so future maintainers understand the contract.
With these practices, you can confidently combine PureScript’s pure functional core with the rich ecosystem of JavaScript libraries.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.