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 published from the GitHub repository. They need Rust 1.99 or newer.
[dependencies]
quickjs-oxide = { git = "https://github.com/pocket-nexus/quickjs-oxide" }
quickjs-oxide-host = { git = "https://github.com/pocket-nexus/quickjs-oxide" }Everything an embedder uses lives in one module:
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.
let runtime = Runtime::new_with_host_services(SystemHost::default());
let mut 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.
// 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.
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) and the thrown value waits on the context. take_exception returns it and clears it.
match context.eval("null.length") {
Err(RuntimeError::Exception) => {
// The thrown value waits on the context until it is taken.
let thrown = context.take_exception()?.expect("a pending exception");
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.
// 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 the repository, or download a prebuilt binary from the GitHub releases:
cargo install --locked --git https://github.com/pocket-nexus/quickjs-oxide quickjs-oxide-cliSave a script as double.js:
const doubled = [1, 2, 3].map((x) => x * 2).join();
console.log(doubled);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 modulesqjs 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
- Values and calls: every
Valuevariant, strings, BigInts, symbols, objects, arrays, calls and constructors. - Host services: implement
Hostto route output, time, randomness and file reads. - Modules: install a
ModuleLoaderand run ES modules. - Promises and jobs: run the job queue and read promise results.
- Errors and limits:
RuntimeError, heap caps and stack limits. - Use the qjs command line: run scripts and modules with
qjs, which follows QuickJS'sqjs. examples/embed.rs: this page's code as one runnable file (cargo run --example embed).