Calling JavaScript from PureScript with foreign import and Effect
Bind JavaScript functions in PureScript with foreign import and Effect, then verify the generated output because FFI types are erased at runtime.
22 Dec 2025, 00:49 UTC

JavaScript works until it does not
You need to call a JavaScript function from PureScript—perhaps localStorage.setItem, a browser API, or a function in an existing foreign.js file. PureScript’s type checker catches many mistakes, but it cannot read your JavaScript. A misspelled export name or a mismatched argument type can compile cleanly and fail only when the generated JavaScript runs.
The useful takeaway is to treat the FFI boundary as an explicit contract: declare JavaScript functions with foreign import, mark side effects with the Effect monad, keep the JavaScript in a separate file, and verify by inspecting the compiler’s output rather than assuming the types enforce themselves at runtime.
Declare the boundary with foreign import
FFI, or Foreign Function Interface, is the mechanism PureScript uses to call code written in another language. The foreign import declaration binds a JavaScript value to a PureScript name and gives it an explicit PureScript type. The Effect monad marks computations that perform side effects, so calling impure JavaScript remains visible in your types.
module Main where
import Effect (Effect)
import Effect.Console (log)
foreign import greet :: String -> Effect String
main :: Effect Unit
main = do
message <- greet "World"
log message
Here greet is implemented in JavaScript. Because it returns Effect String, PureScript knows the call is impure and must be sequenced inside do notation.
Implement the JavaScript side
Create a JavaScript file next to your PureScript sources. The compiler does not type-check this file, so the export names and argument shapes are your responsibility.
// foreign.js
"use strict";
exports.greet = function(name) {
return "Hello, " + name;
};
PureScript looks for these exports when it links the FFI binding. If you rename exports.greet to exports.sayHello, the PureScript code still compiles, but the program fails at runtime when it tries to invoke the missing export.
Build and verify
Run these commands from your project root. You need normal write access to the directory; spago init can overwrite an existing spago.dhall, and package installation requires network access.
spago init— scaffolds a Spago project if one does not already exist.spago install purescript-effect purescript-effect-console— adds the Effect libraries.- Place
Main.pursinsrc/andforeign.jsin the project root orsrc/, according to your Spago configuration. spago build— compiles PureScript to JavaScript in theoutput/directory.
Expected result: spago build exits successfully and generates output/. Open the generated file for your module and search for greet. Confirm the generated code calls the JavaScript export you defined in foreign.js. If the export name is wrong, the build still succeeds; the error appears only when the bundled JavaScript runs.
What the compiler cannot see
FFI type annotations are erased after code generation. The compiler cannot validate the shape of JavaScript objects, even when you use row-polymorphic records to describe them. It also cannot check that your JavaScript file exports the functions you imported, or that those functions handle the arguments PureScript sends them.
Effects launched through FFI must be managed explicitly. If you start side effects in JavaScript and forget to sequence them through Effect, you can leak or duplicate work and introduce subtle ordering bugs.
Keep the boundary thin
Wrap each FFI call in a small PureScript function so the rest of your application stays pure. Treat foreign.js as a sealed boundary with clear exports, and add a runtime test that loads the generated JavaScript and exercises each binding. That test is your safety net, because PureScript’s type checker is not.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.