Rust in. Readable JavaScript out.
Rust.js compiles Rust to readable JavaScript. Built on rustc and inspired by Scala.js and ReScript, it keeps Rust's type system, traits, ownership, borrow checking, and compiler diagnostics while replacing code generation with a JavaScript backend.
The goal is simple:
Write Rust. Get readable JavaScript that works with every tool you already use.
Read the guide: how to write a JavaScript app in Rust.js, as it is today, from a first Vite app to bindings, React and the semantics.
Read the guide: how to write a JavaScript app in rust-js, each example's Rust beside the JavaScript it becomes.
Rust gives us a powerful way to describe programs and catch mistakes before they run. JavaScript gives us the runtime and ecosystem in which much of our software already lives: browsers, React, npm packages, and tools such as Vite and Bun.
We want to use them together. Write a React component in Rust, import a JavaScript package, call a browser API, and inspect the result in DevTools. Let Rust check the program and let JavaScript run it.
WebAssembly provides a way to run compiled Rust in the browser. Rust.js explores a different compilation target: JavaScript itself. Its output is ordinary ES modules and JSX that existing JavaScript applications can import, debug, and build with their usual tools.
The longer-term motivation is Rust across the application stack. A server can
use native Rust while a frontend uses Rust.js, with shared types and data
models describing the contract between them. Support for
#[derive(Serialize, Deserialize)] is one step toward that: for supported
types and #[serde(...)] attributes, generated code writes and reads JSON
with serde_json's behavior, including its deserialization errors.
A React component written in Rust:
pub fn App() -> JSX::Element {
let (count, set_count) = use_state(|| 0);
jsx! {
<button onClick={move |_| set_count.update(|count| count + 1)}>
{"Count is "}{count}
</button>
}
}becomes:
export function App() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((count) => (count + 1) | 0)}>
Count is {count}
</button>
);
}Imports omitted; see the React guide for a complete example.
The component remains a function, state remains a React hook, and the event
handler remains an arrow function. The | 0 keeps the addition within a signed
32-bit integer, following Rust.js's release-style integer arithmetic.
rustc handles parsing, macro expansion, name resolution, types, traits, ownership, lifetimes, and borrow checking. Rust.js uses its checked program representation to generate JavaScript.
A program must pass Rust's checks before Rust.js emits output. Passing those checks is necessary, but the program must also use constructs that Rust.js supports. Unsupported constructs produce compiler errors.
You should be able to open the generated file, understand it, debug it, and call it from JavaScript. Names, module structure, formatting, and source maps all matter because people will work with this code.
That starts with representations a JavaScript programmer would recognize:
| Rust | JavaScript |
|---|---|
struct |
object |
| tuple | array |
| closure | arrow function |
Vec<T> |
Array |
HashMap<K, V> |
Map, for supported key types |
HashSet<T> |
Set, for supported element types |
Option<T> |
usually a value or undefined; generic code boxes ambiguous values |
async / .await |
async / await, with promises |
| module | ES module |
Runtime helpers are imported where the required behavior needs them, from
@rust-js/std, released with the compiler, as
ReScript's are from @rescript/runtime: import { $index } from "@rust-js/std";.
Each helper should earn its place; readable output is a design constraint
throughout the compiler. What std does that reaches no memory, a
Peekable's next, is std's own body, compiled from the MIR the toolchain
ships into the same package, imported the same way
(ADR 0561).
Rust and JavaScript have different runtime models. Reproducing every detail of native Rust would require machinery that can obscure the generated program. Rust.js favors JavaScript's own building blocks and makes the resulting semantic choices explicit.
For example, calling an async function starts its work immediately, up to the
first await, just as it does in JavaScript. Native Rust futures wait until
they are polled. This choice lets JavaScript callers receive an ordinary
promise and lets the JavaScript event loop run the work without a Rust executor.
It also means a future that is never awaited can still have effects.
These differences belong in the design, documentation, and tests. How Rust behaves in Rust.js lists each one, as it is today, and the design decisions record their reasons and costs. When a feature has no supported translation, compilation fails clearly.
Importing JavaScript packages, exporting functions, using DOM APIs, and working with existing bundlers are core concerns. A Rust component should fit into a React application, and its output should remain useful to someone writing JavaScript.
Where behavior should match native Rust, run the same program through both implementations and compare the results. Snapshot the generated JavaScript so changes to its readability can be reviewed too.
Real applications test how those pieces fit together. The playground is written in Rust, compiled by Rust.js, and built with React and Vite. Developing it exercises the same compiler, bindings, and tooling that other applications use.
Rust.js supports a growing subset of Rust aimed at JavaScript applications:
- structs, tuples, enums, pattern matching, and methods
- traits and generics within the supported subset
- closures, iterators,
Option,Result, and common collection operations - strings, formatting, and
async/await - ES modules, JavaScript imports, and DOM bindings generated from WebIDL
- React, JSX, Vite, Fast Refresh, and source maps
- serde-compatible JSON writing and reading,
serde_json::Valueandjson!included (see the design decisions)
Support is specific to each feature. For example, a HashMap iterates in
insertion order, and std::thread::spawn and a u128 in JSON are refused.
How Rust behaves in Rust.js says what each part of Rust
does today and how it differs from native Rust, and
the known limitations what's refused.
Install Bun and Rust, then run these commands from the repository root. The repository pins the required Rust release and components.
bun run setup
bun run build
bun run react-example # React + Vite at localhost:5173
cd examples/vite-react && bunx rust-js build # in another terminal, after you edit src/App.rsAn app's Rust is compiled one way: rust-js build, run in the app beside
its own dev server, next dev or vite, which writes each module's JS and
.d.ts beside its Rust (ADR 0412).
Run bun run typecheck for strict TypeScript checking with the pinned TypeScript 7 compiler.
This checks scripts, tests, generators, and browser code without building Rust.
Bun runs TypeScript directly but does not type-check it. The manual Check workflow
also runs this command. JavaScript dependencies and generated JavaScript are not
subject to strict source checking.
Run bun run test for native Rust comparisons, output snapshots, compiler
diagnostics, source maps, React, browser, and Vite integration tests, the
files side by side (ADR 0104), or
bun run test:serial one after another.
Run bun run fmt to format Rust and playground JSX, or bun run fmt:check to check.
Run bun run to list all tasks.
Roadmap · Design decisions · Examples · React + Vite · DOM bindings · Browser tests · Compiler in WebAssembly
