QuickJS-NG JavaScript engine embedded in the BEAM.
Each runtime is a GenServer holding a persistent JS context.
State, functions, and variables survive across eval/2 and call/3 calls.
iex> {:ok, rt} = QuickBEAM.start()
iex> {:ok, 3} = QuickBEAM.eval(rt, "1 + 2")
iex> QuickBEAM.stop(rt)
:okHandlers
JS code can call Elixir functions via Beam.call and Beam.callSync:
iex> {:ok, rt} = QuickBEAM.start(handlers: %{
...> "greet" => fn [name] -> "Hello, #{name}!" end
...> })
iex> QuickBEAM.eval(rt, ~s[Beam.callSync("greet", "world")])
{:ok, "Hello, world!"}
iex> QuickBEAM.stop(rt)
:okSupervision
Runtimes work as OTP children:
children = [
{QuickBEAM, name: :app, script: "priv/js/app.js", handlers: %{...}},
]
Supervisor.start_link(children, strategy: :one_for_one)Options
:name— GenServer name registration:id— child spec ID (defaults to:name, then module):handlers— map of handler name → function forBeam.call/Beam.callSync:script— path to a JS/TS file evaluated on startup. TypeScript files are automatically transformed. Files withimportstatements are automatically bundled — imports are resolved from the filesystem andnode_modules/, then compiled into a single script via OXC.:memory_limit— maximum JS heap in bytes (default: 256 MB):max_stack_size— maximum JS call stack in bytes (default: 4 MB):max_convert_depth— maximum nesting depth for JS→BEAM value conversion (default: 32):max_convert_nodes— maximum total nodes for JS→BEAM value conversion (default: 10,000)
DOM
Each runtime has a live DOM tree backed by lexbor. JS gets document,
querySelector, createElement, etc. Elixir can read the DOM directly
via dom_find/2, dom_find_all/2, dom_text/2, dom_attr/3, and
dom_html/1 — returning Floki-compatible {tag, attrs, children} tuples.
Summary
Functions
Call a global JavaScript function by name.
Compile JavaScript source to bytecode without executing it.
Get current JS coverage data for a runtime.
Disassemble precompiled bytecode into a %QuickBEAM.Bytecode{} struct.
Compile JavaScript source and disassemble the resulting bytecode.
Get an attribute value from the first element matching a CSS selector.
Find the first element matching a CSS selector in the runtime's DOM.
Find all elements matching a CSS selector in the runtime's DOM.
Serialize the entire DOM tree to an HTML string.
Extract text content from the first element matching a CSS selector.
Evaluate JavaScript code and return the result.
Evaluate TypeScript code by transforming it to JavaScript first.
Get the value of a JS global. Works like eval(rt, "name") but safer —
the name is accessed as a property, not evaluated as code.
List global names defined in the JS context.
Return runtime diagnostics: registered handlers, memory stats, and JS global count.
Load a native addon (.node file) via N-API.
Execute precompiled bytecode from compile/2.
Load an ES module into the runtime.
Return QuickJS memory usage statistics.
Reset the runtime to a fresh JS context. Clears all state and functions.
Send a message to the runtime's JS handler.
Set a JS global variable from Elixir.
Start a new JavaScript runtime.
Stop a runtime and free its resources.
Types
@type js_result() :: {:ok, term()} | {:error, QuickBEAM.JSError.t()}
@type runtime() :: GenServer.server()
Functions
Call a global JavaScript function by name.
Arguments are converted to JS values; the return value is converted back. Promise-returning functions are automatically awaited.
iex> {:ok, rt} = QuickBEAM.start()
iex> QuickBEAM.eval(rt, "function add(a, b) { return a + b }")
iex> QuickBEAM.call(rt, "add", [2, 3])
{:ok, 5}
iex> QuickBEAM.stop(rt)
:okOptions
:timeout— maximum execution time in milliseconds (default: no limit)
@spec compile(runtime(), String.t()) :: {:ok, binary()} | {:error, QuickBEAM.JSError.t()}
Compile JavaScript source to bytecode without executing it.
Returns {:ok, bytecode} where bytecode is a binary that can be loaded
into any runtime with load_bytecode/2. Useful for precompilation, caching,
and transferring compiled code between runtimes or nodes.
Get current JS coverage data for a runtime.
Returns {:ok, %{filename => %{line => hit_count}}}.
Coverage must be enabled via QuickBEAM.Cover.
@spec disasm(binary()) :: {:ok, QuickBEAM.Bytecode.t()} | {:error, String.t()}
Disassemble precompiled bytecode into a %QuickBEAM.Bytecode{} struct.
Does not require a running runtime — creates a temporary QuickJS context internally to parse the binary format.
{:ok, bytecode} = QuickBEAM.compile(rt, "function add(a, b) { return a + b }")
{:ok, %QuickBEAM.Bytecode{}} = QuickBEAM.disasm(bytecode)Also accepts JavaScript source code and a runtime, compiling it first:
{:ok, %QuickBEAM.Bytecode{}} = QuickBEAM.disasm(rt, "function add(a, b) { return a + b }")
@spec disasm(runtime(), String.t()) :: {:ok, QuickBEAM.Bytecode.t()} | {:error, term()}
Compile JavaScript source and disassemble the resulting bytecode.
{:ok, %QuickBEAM.Bytecode{cpool: [%QuickBEAM.Bytecode{name: "add"}]}} =
QuickBEAM.disasm(rt, "function add(a, b) { return a + b }")
Get an attribute value from the first element matching a CSS selector.
Returns nil if the element or attribute is not found.
{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval(rt, ~s[document.body.innerHTML = '<a href="/page">link</a>'])
{:ok, "/page"} = QuickBEAM.dom_attr(rt, "a", "href")
Find the first element matching a CSS selector in the runtime's DOM.
Returns the element as a Floki-compatible {tag, attrs, children} tuple,
or nil if no match is found. This reads the live DOM tree directly from
the native layer — no JS execution or HTML re-parsing.
{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval(rt, "document.body.innerHTML = '<p class="intro">Hello</p>'")
{:ok, {"p", [{"class", "intro"}], ["Hello"]}} = QuickBEAM.dom_find(rt, "p.intro")
Find all elements matching a CSS selector in the runtime's DOM.
Returns a list of Floki-compatible {tag, attrs, children} tuples.
{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval(rt, ~s[document.body.innerHTML = '<ul><li>a</li><li>b</li></ul>'])
{:ok, items} = QuickBEAM.dom_find_all(rt, "li")
length(items) # => 2
Serialize the entire DOM tree to an HTML string.
{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval(rt, "document.body.innerHTML = '<p>Hello</p>'")
{:ok, html} = QuickBEAM.dom_html(rt)
Extract text content from the first element matching a CSS selector.
{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval(rt, "document.body.innerHTML = '<h1>Title</h1>'")
{:ok, "Title"} = QuickBEAM.dom_text(rt, "h1")
Evaluate JavaScript code and return the result.
Top-level await is supported.
iex> {:ok, rt} = QuickBEAM.start()
iex> QuickBEAM.eval(rt, "40 + 2")
{:ok, 42}
iex> QuickBEAM.eval(rt, "await Promise.all([1, 2].map(x => Promise.resolve(x)))")
{:ok, [1, 2]}
iex> QuickBEAM.stop(rt)
:okOptions
:timeout— maximum execution time in milliseconds (default: no limit). If exceeded, the JS execution is interrupted and an error is returned. The runtime remains usable after a timeout.QuickBEAM.eval(rt, "while(true) {}", timeout: 1000) # => {:error, %QuickBEAM.JSError{message: "interrupted", ...}}:vars— a map of variable names to values, available in the code as globals. Values are converted using the standard BEAM→JS conversion. Variables are automatically cleaned up after evaluation, even if the code throws an error.QuickBEAM.eval(rt, "name.toUpperCase()", vars: %{"name" => "quickbeam"}) # => {:ok, "QUICKBEAM"} QuickBEAM.eval(rt, "items.map(i => i.price * i.qty).reduce((a, b) => a + b, 0)", vars: %{"items" => [%{"price" => 10, "qty" => 3}, %{"price" => 5, "qty" => 2}]}) # => {:ok, 40}
Evaluate TypeScript code by transforming it to JavaScript first.
Equivalent to OXC.transform!/2 followed by eval/3, but in a single call.
iex> {:ok, rt} = QuickBEAM.start()
iex> QuickBEAM.eval_ts(rt, "const x: number = 40 + 2; x")
{:ok, 42}
iex> QuickBEAM.stop(rt)
:okOptions
Accepts the same options as eval/3 (e.g., :timeout).
Get the value of a JS global. Works like eval(rt, "name") but safer —
the name is accessed as a property, not evaluated as code.
Returns the value converted to Elixir terms. For objects, returns a map of enumerable own properties. For functions, returns a map with metadata.
Examples
QuickBEAM.get_global(rt, "myVar")
{:ok, 42}
QuickBEAM.get_global(rt, "myObj")
{:ok, %{"x" => 1, "y" => 2}}
QuickBEAM.get_global(rt, "nonexistent")
{:ok, nil}
@spec globals( runtime(), keyword() ) :: {:ok, [String.t()]} | {:error, QuickBEAM.JSError.t()}
List global names defined in the JS context.
By default returns all globalThis property names. Pass user_only: true
to exclude JS builtins and QuickBEAM internals — only names defined by
your scripts.
Examples
{:ok, all} = QuickBEAM.globals(rt)
# ["Array", "Boolean", "Buffer", "Object", "console", "myVar", ...]
{:ok, mine} = QuickBEAM.globals(rt, user_only: true)
# ["myVar", "myFunc"]
Return runtime diagnostics: registered handlers, memory stats, and JS global count.
Load a native addon (.node file) via N-API.
The addon is loaded with dlopen and its napi_register_module_v1 (or
napi_module_register) entry point is called. Returns the addon's exports
as an Elixir term.
Options
:as- set the addon's exports as a global JS variable with this name, making the functions callable fromeval/3andcall/3
Examples
QuickBEAM.load_addon(rt, "/path/to/addon.node")
QuickBEAM.load_addon(rt, "/path/to/crc32.node", as: "crc32")
QuickBEAM.eval(rt, "crc32.crc32('hello')")
Execute precompiled bytecode from compile/2.
The bytecode runs in the current runtime's context, with access to all globals, handlers, and builtins.
Load an ES module into the runtime.
iex> {:ok, rt} = QuickBEAM.start()
iex> code = "export function add(a, b) { return a + b; }"
iex> QuickBEAM.load_module(rt, "math", code)
:ok
iex> QuickBEAM.stop(rt)
:ok
Return QuickJS memory usage statistics.
Reset the runtime to a fresh JS context. Clears all state and functions.
iex> {:ok, rt} = QuickBEAM.start()
iex> QuickBEAM.eval(rt, "globalThis.x = 42")
iex> QuickBEAM.reset(rt)
:ok
iex> QuickBEAM.eval(rt, "typeof x")
{:ok, "undefined"}
iex> QuickBEAM.stop(rt)
:ok
Send a message to the runtime's JS handler.
The message is delivered to the callback registered via Beam.onMessage
in JS. If no handler is registered, the message is silently discarded.
Set a JS global variable from Elixir.
The value is converted using the standard BEAM→JS conversion (no JSON).
Examples
QuickBEAM.set_global(rt, "config", %{"theme" => "dark", "limit" => 100})
{:ok, "dark"} = QuickBEAM.eval(rt, "config.theme")
QuickBEAM.set_global(rt, "items", [1, 2, 3])
{:ok, 3} = QuickBEAM.eval(rt, "items.length")
@spec start(keyword()) :: GenServer.on_start()
Start a new JavaScript runtime.
Returns {:ok, pid} on success.
Options
:name— register the GenServer under this name:handlers—%{String.t() => function}map forBeam.call/Beam.callSync:script— path to a JS/TS file to evaluate on startup (auto-bundles imports):apis— which API surfaces to load (default:[:browser]):browser— Web APIs (fetch, DOM, WebSocket, crypto, streams, …):node— Node.js compat (process, path, fs, os)[:browser, :node]— bothfalse— bare QuickJS engine, no polyfills
:define—%{String.t() => term()}of globals to inject before the script runs. Values are JSON-encoded. Useful for passing config withoutBeam.callSync.QuickBEAM.start(script: "build.ts", define: %{"outputDir" => "/tmp/site"}):memory_limit— maximum JS heap in bytes (default: 256 MB):max_stack_size— maximum JS call stack in bytes (default: 8 MB):max_convert_depth— maximum nesting depth for JS→BEAM value conversion (default: 32):max_convert_nodes— maximum total nodes for JS→BEAM value conversion (default: 10,000)
@spec stop(runtime()) :: :ok
Stop a runtime and free its resources.