How to Use js-sys for JavaScript APIs from Rust

Web
Use `js-sys` to access JavaScript APIs by importing the specific function or object you need from the crate and calling it with `wasm-bindgen`'s `JsValue` types, ensuring your project is configured with the `wasm-bindgen` and `wasm-bindgen-macro` dependencies.

When Rust meets the JavaScript host

You are building a WebAssembly module in Rust. Your logic is solid, your types are strict, and your memory management is zero-cost. Then you hit a wall. You need to log a debug message to the browser console, or grab the current timestamp, or call Math.random. You could write raw foreign function interface code, wrestling with pointers and memory layouts. Or you could reach for js-sys.

This crate gives you a safe, typed Rust interface to the entire JavaScript standard library. You call JavaScript APIs like they are native Rust functions, without touching unsafe. The crate handles the boundary crossing, the type conversions, and the error propagation. You get the power of the JavaScript host with the safety guarantees of Rust.

The generated map of JavaScript

JavaScript runs in a host environment. The browser provides a massive global object with thousands of methods. js-sys is a pre-generated map of that territory. The wasm-bindgen team scans the JavaScript specification and generates Rust bindings for every standard API. You get structs for Date, Math, JSON, and Array. You get methods that match the JavaScript signatures.

The crate lives on the boundary. When you import a type from js_sys, you are not importing a Rust struct that owns data. You are importing a handle to a JavaScript object. The actual data lives in the JavaScript heap. Rust holds a reference to that handle. When the handle goes out of scope, the reference count drops. When the count hits zero, the JavaScript garbage collector reclaims the memory.

This design eliminates a whole class of bugs. You cannot accidentally free memory that JavaScript still needs. You cannot create dangling pointers. The wasm-bindgen runtime tracks every object that crosses the boundary. If you try to use a handle after the JavaScript object is gone, the runtime catches it and panics. The cost is a small amount of overhead for tracking, but the safety gain is massive.

Minimal example: Logging to the console

The most common first step is logging. You want to verify that your Rust code is running and passing data correctly. The js-sys crate exposes the console object as a module with static methods.

use wasm_bindgen::prelude::*;
use js_sys::console;

/// Greet a user and log the message to the JavaScript console.
#[wasm_bindgen]
pub fn greet(name: &str) {
    // Convert the Rust string to a JsValue for the boundary crossing
    let msg = JsValue::from_str(&format!("Hello from Rust, {}", name));
    
    // Call the JavaScript console.log with exactly one argument
    console::log_1(&msg);
}

You will notice the method is log_1, not log. JavaScript functions often accept a variable number of arguments. Rust requires fixed arity. The js-sys crate generates separate methods for each argument count: log_0, log_1, log_2, up to log_15. This keeps the call site explicit and type-safe. You cannot accidentally pass two arguments to a function that expects one. The compiler rejects the mismatch before you ever run the code.

Convention aside: The community prefers the explicit _1 form over the generic call method for standard APIs. The _1 form gives you compile-time checks for argument types. The call method accepts &[JsValue] and returns a JsValue. It is dynamic and loses type safety. Use log_1 when you know the argument count. Use call only when you are building a dynamic wrapper.

How the boundary crossing works

When you compile this code, wasm-bindgen inspects the #[wasm_bindgen] attribute and the js_sys imports. It generates a JavaScript glue file. This glue file contains the actual JavaScript function calls. At runtime, when your WASM module calls greet, the glue code intercepts the call.

The process follows a strict path. Your Rust code prepares the arguments. The wasm-bindgen runtime allocates space in the JavaScript heap for each argument. It copies the data from the WASM linear memory into the JavaScript heap. It invokes the JavaScript function with the new objects. The JavaScript function returns a value. The runtime copies the result back into the WASM linear memory or hands you a handle to a JavaScript object. The glue code then returns control to your Rust function.

You never see the glue. You just see Rust calling Rust. The abstraction is complete. The trade-off is performance. Crossing the boundary allocates memory. It copies data. It invokes the JavaScript engine. Doing this in a tight loop kills performance. Keep your hot loops in Rust. Cross the boundary only when you need to interact with the host.

Section closer: Don't cross the boundary in a hot loop. Batch your data and cross once.

Realistic example: Dates and Math

Real applications need more than logging. You need timestamps, random numbers, and JSON serialization. js-sys provides bindings for Date and Math. These bindings map JavaScript constructors and static methods to Rust static methods.

use wasm_bindgen::prelude::*;
use js_sys::{Date, Math};

/// Get the current timestamp and a random number.
#[wasm_bindgen]
pub fn get_timestamp_and_random() -> (f64, f64) {
    // Create a new JavaScript Date object representing "now"
    let now = Date::new_0();
    
    // Extract the timestamp as a Rust f64
    let timestamp = now.get_time();
    
    // Call Math.random() via the js_sys wrapper
    let random = Math::random();
    
    (timestamp, random)
}

JavaScript constructors map to new_0, new_1, etc. in js-sys. The new keyword in JS is just a function call with a special receiver. Rust treats it as a static method on the struct. Date::new_0() creates a new Date object with the current time. Date::new_1(&JsValue::from(12345)) creates a Date from a timestamp. The naming convention is consistent across all constructors.

The Math object is a singleton. It has no constructor. You access its methods directly via static methods on the Math struct. Math::random() returns an f64. Math::sin(x) takes an f64 and returns an f64. The types match the JavaScript specification exactly.

Convention aside: Date objects are cheap to clone. Cloning a Date in js-sys does not copy the underlying JavaScript object. It creates a new handle pointing to the same object. The reference count bumps. When you drop the clone, the count drops. This is different from Rust's Clone trait for owned types. Treat js-sys types as references, not values.

The universal adapter: JsValue

Sometimes you need to call a JavaScript function that js-sys does not expose. Or you need to pass a dynamic value that changes type at runtime. JsValue is the universal adapter. It wraps any JavaScript value. You can convert Rust types into JsValue and pass them to JavaScript functions. You can retrieve results as JsValue and convert them back to Rust types.

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

/// Convert a JsValue to a string if possible.
#[wasm_bindgen]
pub fn to_string(value: JsValue) -> String {
    // Try to cast the JsValue to a string
    if let Some(s) = value.as_string() {
        return s;
    }
    
    // Fall back to calling toString() on the object
    let to_string = value.get("toString");
    if let Ok(func) = to_string.dyn_into::<Function>() {
        if let Ok(result) = func.call1(&value, &JsValue::NULL) {
            if let Some(s) = result.as_string() {
                return s;
            }
        }
    }
    
    // Return a placeholder if conversion fails
    "[Object]".to_string()
}

JsValue provides methods like as_f64(), as_string(), is_undefined(), and is_null(). These methods return Option<T>. They check the type of the underlying JavaScript value. If the type matches, they return the value. If not, they return None. This allows you to handle dynamic JavaScript data safely in Rust.

You can also call methods dynamically using JsValue::call(). This method takes a this value and an array of arguments. It returns a Result<JsValue, JsValue>. The error variant contains the JavaScript exception object. You can inspect the exception to debug failures.

Convention aside: Use JsValue::from() for simple conversions. JsValue::from(42) creates a number. JsValue::from(true) creates a boolean. JsValue::from_str("hello") creates a string. The generic from method is idiomatic and concise. Reserve JsValue::from_str for cases where you need to be explicit about string allocation.

Pitfalls and runtime panics

JavaScript APIs can throw exceptions. Rust functions can panic. When a JavaScript function throws, wasm-bindgen catches it and converts it into a Rust panic. If you don't handle this, your WASM module crashes. Some js-sys methods return Result<T, JsValue> to indicate potential failure. Always check the return type. If a method returns JsValue, it might be an error object.

If you call a method that throws and ignore the result, the browser console shows a panic message from wasm-bindgen. The stack trace points to the glue code, not your Rust source. Use the ? operator or match to handle Result types gracefully. You can catch the panic and convert it back to a JavaScript error if you need to propagate it to the host.

use wasm_bindgen::prelude::*;
use js_sys::JSON;

/// Parse a JSON string and return the result.
#[wasm_bindgen]
pub fn parse_json(input: &str) -> Result<JsValue, JsValue> {
    // Convert the Rust string to a JsValue
    let js_input = JsValue::from_str(input);
    
    // Call JSON.parse, which returns a Result
    JSON::parse(&js_input)
}

The JSON::parse method returns Result<JsValue, JsValue>. The error variant contains the SyntaxError object thrown by JavaScript. You can return this error to the caller or handle it in Rust. This pattern is common for APIs that can fail. JSON.stringify also returns a Result. Date::new can return a Result if the arguments are invalid.

Another pitfall is environment dependency. js-sys assumes a JavaScript runtime. If you run this code in a non-web environment without a polyfill, it fails. The wasm-bindgen test runner provides a mock environment for testing. You can use wasm-bindgen-test to run your Rust code in a headless browser. This ensures your bindings work correctly before you deploy.

Section closer: Treat every JavaScript call as potentially fallible. Check the return type. Handle the errors.

Decision: js-sys vs web-sys vs extern

You have three options for calling JavaScript from Rust. Each serves a different purpose. Choose the right tool based on your needs.

Use js-sys when you need to call standard JavaScript APIs like console, Date, Math, or JSON. Use web-sys when you need to manipulate the DOM, handle events, or access browser-specific features like localStorage. Use raw wasm-bindgen extern blocks when you are calling custom JavaScript functions defined in your own glue code, not standard library APIs.

Reach for js-sys over manual FFI because it eliminates pointer arithmetic and provides compile-time checks for argument types. Reach for web-sys when you need DOM access because it provides typed bindings for every HTML element and CSS property. Reach for extern when you are building a bridge to a custom JavaScript library that is not part of the standard.

Section closer: Stick to the standard crates. They are maintained, tested, and safe. Only write custom bindings when you have no other choice.

Where to go next