Embed quickjs-oxide

quickjs-oxide is a JavaScript engine for Rust applications. It is written in Rust with no unsafe code: every crate in the workspace sets unsafe_code = "forbid". ECMA-262 and Test262 decide its behavior; QuickJS 2026-06-04 decides what the specification leaves to the implementation, such as error messages and the printer.

An application creates a Runtime, evaluates JavaScript through its Context, and reads the results as Rust values. The engine reaches the outside world only through a Host (output, clock, time zone, random seed, file reads, promise-rejection events), so the application decides what scripts can see.

What you want to do Where to start
Run a first script from Rust this page, sections 1 to 4
Read, build and pass JavaScript values Values and calls
Control output, time, randomness and files Host services
Load ES modules from your own sources Modules
Drive promises and async code Promises and jobs
Handle exceptions, cap memory and stack Errors and limits
Run the engine in a browser WebAssembly
Run scripts and modules from the command line Use the qjs command line
Check which JavaScript features are supported JavaScript support

The crates

Crate Use it for
quickjs-oxide The engine and its embedding API, under quickjs_oxide::engine::api::core.
quickjs-oxide-host SystemHost: the system clock, the host's time-zone rules (TZ or /etc/localtime), stdout and stderr, files.
quickjs-oxide-web-host WebHost: the browser's clock, time zone and random source, for wasm32 builds.
quickjs-oxide-cli The qjs command line, built on the two crates above.

1. Add the crates

The crates are on crates.io, with their API documentation on docs.rs. They need Rust 1.99 or newer.

toml
[dependencies]
quickjs-oxide = "0.4"
quickjs-oxide-host = "0.4"

Everything an embedder uses lives in one module:

examples/embed.rsrust
use quickjs_oxide::engine::api::core::{JsString, QjsHelpers, Runtime, RuntimeError, Value};
use quickjs_oxide_host::SystemHost;

2. Evaluate a script

A Runtime owns one heap, one realm and one job queue. new_context returns the handle of its realm, and eval compiles and runs a script and returns its completion value.

examples/embed.rsrust
let runtime = Runtime::new_with_host_services(SystemHost::default());
let context = runtime.new_context()?;

let value = context.eval("[1, 2, 3].map((x) => x * 2).join()")?;

let Value::String(text) = value else {
    panic!("expected a string, got {value:?}");
};
assert_eq!(text.to_utf8_lossy(), "2,4,6");

Runtime::new_with_host_services takes the host. SystemHost from quickjs-oxide-host reads the system clock and the local time zone; Runtime::new() uses a built-in host that reports UTC.

A runtime is single-threaded: Runtime and Context are reference-counted handles that stay on the thread that created them. Separate runtimes share nothing, so an application that needs isolation creates one runtime per tenant.

A fresh realm has the ECMAScript built-ins and nothing else. The qjs helpers add print, console.log and scriptArgs; their output goes through the host's write.

examples/embed.rsrust
// A fresh realm has the standard built-ins only.
assert_eq!(
    context.eval("typeof console")?,
    Value::String(JsString::from_static("undefined"))
);

// Add qjs's print and console.log; they write through the host (stdout here).
context.install_qjs_helpers(&QjsHelpers::default())?;
context.eval("console.log('hello from JavaScript')")?;

3. Read the result

eval returns a Value. Primitives arrive as Rust data: numbers as Int(i32) or Float(f64), strings as JsString, BigInts as JsBigInt. Objects and symbols arrive as handles (ObjectRef, SymbolRef) that keep the JavaScript value alive while Rust holds them.

examples/embed.rsrust
assert_eq!(context.eval("6 * 7")?, Value::Int(42));
assert_eq!(context.eval("0.5 + 0.25")?, Value::Float(0.75));
assert_eq!(context.eval("1 < 2")?, Value::Bool(true));
assert_eq!(context.eval("var unused = 1")?, Value::Undefined);

// Numbers: `as_number` reads both `Int` and `Float`.
let total = context.eval("[1.5, 2, 3].reduce((a, b) => a + b)")?;
assert_eq!(total.as_number(), Some(6.5));

// Objects come back as handles; read their properties through the context.
let point = context.eval("({ x: 3, y: 4 })")?;
let point = point.as_object().expect("an object");
let x = runtime.intern_property_key("x")?;
assert_eq!(context.get_property(point, &x)?, Value::Int(3));

A property name is a PropertyKey. Runtime::intern_property_key makes one from a Rust string, and PropertyKey::index makes an array index. Values and calls covers every variant and conversion.

When the script throws, eval returns Err(RuntimeError::Exception(value)) with the thrown value.

examples/embed.rsrust
match context.eval("null.length") {
    Err(RuntimeError::Exception(thrown)) => {
        // The error carries the thrown value.
        let message = context.to_js_string(&thrown)?;
        assert_eq!(
            message.to_utf8_lossy(),
            "TypeError: cannot read property 'length' of null"
        );
    }
    other => panic!("expected an exception, got {other:?}"),
}

4. Call a JavaScript function from Rust

A function is an object; Runtime::as_callable turns an ObjectRef into a CallableRef when the object is callable. Context::call takes the function, the this value and the arguments.

examples/embed.rsrust
// Define a function in JavaScript.
context.eval(
    "function greet(name, times) { return ('hi ' + name + ' ').repeat(times).trim(); }",
)?;

// Look it up on the global object and check that it is callable.
let global = context.global_object()?;
let key = runtime.intern_property_key("greet")?;
let Value::Object(object) = context.get_property(&global, &key)? else {
    panic!("greet is not an object");
};
let greet = runtime.as_callable(&object)?.expect("greet is callable");

// Call it with `this` undefined and two arguments.
let name = Value::String(JsString::try_from_utf8("Ada")?);
let result = context.call(&greet, Value::Undefined, &[name, Value::Int(2)])?;
let text = context.to_js_string(&result)?;
assert_eq!(text.to_utf8_lossy(), "hi Ada hi Ada");

JavaScript reaches the application only through the Host trait, described in Host services.

5. Run the same script with qjs

The qjs command line runs files and expressions with the same engine, so a script can be tried before it is embedded. Install it from crates.io, or download a prebuilt binary from the GitHub releases:

shell
cargo install --locked quickjs-oxide-cli

Save a script as double.js:

js
const doubled = [1, 2, 3].map((x) => x * 2).join();
console.log(doubled);
shell
qjs double.js                      # 2,4,6
qjs --print-result -e '6 * 7'      # 42
qjs -e 'null.length'               # prints the TypeError, exits with status 1
qjs module.mjs                     # .mjs files run as ES modules

qjs installs the same helpers as install_qjs_helpers, a file module loader and promise-rejection tracking, then drains the job queue before it exits. Use the qjs command line covers its options and how it follows QuickJS's qjs.

Where to go next