Skip to content

Rust for JavaScript and TypeScript Developers

If you write JavaScript or TypeScript, you already know more Rust than you think. Rust has closures that work a lot like arrow functions, async/await, iterator methods with familiar names like map, filter, and find, destructuring, and one command-line tool (Cargo) that does what npm does. If you use TypeScript, you’re also used to a compiler that catches mistakes before your code runs. Rust’s compiler does the same thing, only stricter.

So what’s actually new? Three things. Rust has no garbage collector: every value has one owner, and the compiler checks ownership and borrowing before your program runs. Rust has no undefined or null: a value that might be missing has the type Option. And Rust has no exceptions: a function that can fail returns a Result. Most of your learning time will go into these three ideas.

Why do JavaScript developers learn Rust? Usually for one of these reasons: you want a fast web backend or command-line tool that ships as a single file, you want to speed up a hot spot in the browser with WebAssembly, you want to write a native Node.js addon, or you’re curious about the tools you already use. SWC, Biome, Oxc, Rolldown, Rspack, Turbopack, Lightning CSS, and Deno are all written in Rust.

This page maps what you know from JavaScript and TypeScript to Rust, so you can spend your time on the parts that are really new. For a general comparison with C++, Go, and Python instead, see Rust vs Other Languages.

Language concepts: JavaScript and TypeScript to Rust

Section titled “Language concepts: JavaScript and TypeScript to Rust”

Use this table as a phrase book. The last column links to the lesson on this site that explains the Rust side.

JavaScript / TypeScript Rust Learn more
let and const let (can’t change) and let mut (can change). A JS const object can still be changed inside; a Rust let value can’t Variables
number You pick a size: i32, i64, u32, usize, f64, … Integers and floats are different types Data Types
bigint i128 / u128, or the num-bigint crate for any size Data Types
string String (owned, growable) and &str (a borrowed view of text) Strings
boolean bool Booleans
Template literals format!("Hi {name}") and println!("Hi {name}") Output
any, unknown No any. Use generics, an enum, or serde_json::Value for JSON of unknown shape Generics, Serde & JSON
interface or type describing an object’s shape struct Structs
interface describing methods trait Traits
Structural typing (same shape = same type) Nominal typing: two structs with the same fields are still different types Structs
Union types and discriminated unions enum, where each variant can carry its own data Enums
String literal types like "small" or "large" An enum with plain variants: enum Size { Small, Large } Enums
undefined, null, optional property x?: T Option<T>: either Some(value) or None Option
?. and ?? ? (inside a function that returns Option), .map(...), .and_then(...), .unwrap_or(...) Option
Non-null assertion x! .unwrap() or .expect("reason"), which stop the program if the value is None Panic
throw, try/catch, rejected Promises Result<T, E> + the ? operator Result
class MyError extends Error Your own error enum (often with thiserror); anyhow in applications Custom Errors, anyhow & thiserror
class with fields and methods struct for the data + an impl block for the methods Structs
extends (inheritance) No inheritance. Use composition (a struct inside a struct) + traits with default methods Traits, Design Patterns
class Dog implements Animal impl Animal for Dog Traits
An array of different classes that share an interface Vec<Box<dyn Animal>> (a trait object) Trait Objects
Generics like T extends HasId Generics with trait bounds: T: HasId Generics
Arrow functions and callbacks Closures, described by the traits Fn, FnMut, FnOnce Closures, Fn, FnMut & FnOnce
this An explicit self, &self, or &mut self parameter. No surprises about what this points to Structs
Destructuring const { x, y } = point Patterns: let Point { x, y } = point; (also in match and function parameters) Pattern Matching
Spread { ...user, admin: true } Struct update syntax: User { admin: true, ..user } Structs
switch, or if chains on typeof / kind match (the compiler checks you covered every case) match
=== ==, which compares values (through the PartialEq trait). No type coercion, ever Common Traits
for...of for item in &items for
map, filter, reduce, find, some, every Iterator methods: map, filter, fold, find, any, all, then collect Iterators
Generators (function*) and Symbol.iterator Implement the Iterator trait Custom Iterators
Promise<T> A Future with Output = T. Unlike a Promise, it does nothing until awaited Async/Await
async / await async fn and .await, plus a runtime like tokio to run them Async/Await, Async in Depth
Promise.all tokio::join!, or join_all from the futures crate Async in Depth
setTimeout tokio::time::sleep in async code, std::thread::sleep otherwise Async/Await
Web Workers, worker_threads Real threads: std::thread::spawn, or rayon for parallel loops Concurrency
ES modules: import, export Modules: mod, use, and pub Modules
JSON.parse / JSON.stringify serde_json with #[derive(Serialize, Deserialize)] Serde & JSON
console.log println!, and dbg! for quick debugging. Use {:?} for types that derive Debug Output
readonly, as const, Object.freeze Not needed: everything is read-only unless you write mut Variables
Decorators Attributes like #[derive(Debug)] and macros Macros
Number("abc") gives NaN "abc".parse::<i32>() returns an Err you have to handle Type Casting, Conversions
Garbage collector Ownership and borrowing, checked at compile time Ownership, Borrowing
JavaScript Rust Learn more
Array Vec<T> (every item has the same type) Vectors
Fixed-length array, or a TS tuple [string, number] [T; N] for a fixed length, a tuple (String, i32) for mixed types Arrays, Tuples
Map, or an object used as a dictionary (Record<string, T>) HashMap<K, V> HashMaps
A map with sorted keys BTreeMap<K, V> HashMaps
Set HashSet<T> (or BTreeSet<T> for sorted order) HashSet
Array used as a queue (push + shift) VecDeque<T> (push_back / pop_front, both fast) VecDeque & BinaryHeap
Array used as a stack (push + pop) Vec<T> with push / pop Vectors
Uint8Array, Buffer Vec<u8>, or a byte slice &[u8] Slices
WeakMap, WeakRef Weak<T> (from Rc or Arc) Weak & Cow

One thing to watch: a JavaScript Map, Set, and plain object remember the order you inserted things. A Rust HashMap and HashSet don’t. If you need sorted order, use BTreeMap; if you need insertion order, the indexmap crate gives you exactly that.

Cargo will feel like home. It’s your package manager, build tool, test runner, and script runner in one, and every Rust project uses it the same way.

JavaScript Rust Notes
node + npm (or pnpm, yarn) cargo Builds, runs, tests, adds packages, publishes. See Cargo & Crates
package.json Cargo.toml
package-lock.json Cargo.lock Created for you
node_modules/ No per-project folder. Downloads are cached once in ~/.cargo, build output goes to target/
npmjs.com crates.io Every published crate’s API docs are on docs.rs
npm install zod cargo add serde
npm init cargo new my_app (cargo new --lib for a library)
npm run dev / build / test cargo run / cargo build / cargo test Add --release for an optimized build; debug builds are much slower
"scripts" in package.json No scripts section. Use cargo aliases in .cargo/config.toml, or a task runner like just
npx some-tool cargo install some-tool, then run it cargo binstall downloads prebuilt binaries instead of compiling
nvm and .nvmrc rustup and a rust-toolchain.toml file See Rust Tools & Setup
npm / pnpm workspaces (monorepo) A Cargo workspace One Cargo.toml at the top lists the member crates
tsc rustc, called for you by cargo
tsc --noEmit cargo check Checks types without building; much faster
ESLint cargo clippy Hundreds of helpful lints, official
Prettier cargo fmt One standard style for everyone
nodemon, tsx watch cargo watch or bacon Installed with cargo install
Jest / Vitest / Mocha Built-in #[test] and cargo test (cargo nextest is a popular faster runner) See Testing
Jest snapshots insta See Testing Beyond the Basics
jest.mock() mockall, or hand-written fakes that implement your trait
fast-check proptest Property-based testing
c8 / Istanbul (coverage) cargo llvm-cov
JSDoc / TypeDoc /// doc comments in Markdown + cargo doc Code examples in docs are run as tests. See Documentation
VS Code + the TypeScript server VS Code + rust-analyzer, or JetBrains RustRover rust-analyzer gives completion, go-to-definition, and inline errors
npm publish cargo publish

Here is a small Cargo.toml, so you can compare it with a package.json:

[package]
name = "orders-api"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

A version like "1" means the same as "^1" in package.json: any compatible 1.x release. Features are optional parts of a crate that you switch on; see Cargo Features & cfg.

Libraries and frameworks: “What do we use instead of…?”

Section titled “Libraries and frameworks: “What do we use instead of…?””

Every JavaScript developer asks this first. “We use Express and Prisma. What’s the Rust version?” Here are the closest equivalents. Like npm, crates.io has many choices for most jobs; these are the popular, well-maintained ones.

JavaScript Rust Notes
Express, Fastify, Koa axum (we build one in Project: Web API), actix-web, Rocket axum and actix-web are widely used in production
Express middleware tower layers and the tower-http crate (CORS, compression, tracing, timeouts) axum uses tower directly
fetch, axios reqwest Async by default, with an optional blocking client. See Networking
ws, Socket.IO WebSockets in axum (the ws feature), tokio-tungstenite, or socketioxide (a Socket.IO server)
Apollo Server (GraphQL) async-graphql
gRPC (@grpc/grpc-js) tonic (with prost for protobuf)
Swagger / OpenAPI generators utoipa
jsonwebtoken jsonwebtoken Same name, same idea
bcrypt argon2 or bcrypt
JavaScript Rust Notes
Prisma, TypeORM SeaORM (async, entities, migrations) or Diesel (queries type-checked at compile time) SeaORM feels closest to Prisma and TypeORM
Knex, pg, postgres.js sqlx: you write plain SQL, and the query! macro checks it against your real database at compile time Async, very popular, not an ORM
better-sqlite3 rusqlite, or sqlx with SQLite
Mongoose, the MongoDB driver mongodb (the official driver) No Mongoose-style schemas; you use serde structs
ioredis redis

Rust Databases shows sqlx, Diesel, and SeaORM with working code.

JavaScript Rust Notes
JSON.parse / JSON.stringify serde + serde_json The standard; also does TOML, YAML, and more. See Serde & JSON
Zod, Yup Your struct’s types + serde do most of the work; validator or garde for rules like “at least 8 characters” If the JSON doesn’t match the struct, parsing fails with an error
Lodash Mostly the standard library’s iterators; itertools for extras like unique, sorted_by, and chunk_by
date-fns, Day.js, Luxon chrono or jiff See Date & Time
dotenv dotenvy See Environment & Processes
Commander, yargs clap See Project: CLI with clap
chalk colored or owo-colors
winston, pino tracing (+ tracing-subscriber), or log + env_logger See Logging
uuid uuid
EventEmitter Channels (std::sync::mpsc, or tokio::sync::broadcast for many listeners) See Channels
BullMQ (job queues) tokio::spawn for in-process tasks; apalis for persistent job queues
Regular expressions regex No lookahead or backreferences, by design. See Regex
JavaScript Rust Notes
Speeding up heavy browser code Compile Rust to WebAssembly with wasm-bindgen and wasm-pack See WebAssembly and the section below
React, Vue, Svelte Leptos, Yew, Dioxus (all compile to WebAssembly) All before version 1.0; APIs still change
Electron Tauri: your web frontend + a Rust backend, with much smaller apps Tauri 2 is stable. See GUI Apps
Native Node.js addons (C++ with node-gyp) napi + napi-derive (napi-rs), or neon Ship prebuilt binaries to npm; no C++ needed
Phaser, Three.js games Bevy, macroquad Bevy is pre-1.0 and breaks its API between releases. See Game Development

Side by side: an interface becomes a struct

Section titled “Side by side: an interface becomes a struct”

Here is a TypeScript object with an interface, a method-like function, and a spread copy:

interface User {
name: string;
age: number;
admin: boolean;
}
const greeting = (u: User) => `Hi, I'm ${u.name} (${u.age})`;
const ada: User = { name: "Ada", age: 36, admin: false };
const promoted = { ...ada, admin: true };
console.log(greeting(ada));
console.log(promoted);

In Rust, the shape becomes a struct, and the function becomes a method in an impl block:

#[derive(Debug, Clone)]
struct User {
name: String,
age: u32,
admin: bool,
}
impl User {
// `&self` means "this method reads the user, it doesn't change it".
fn greeting(&self) -> String {
format!("Hi, I'm {} ({})", self.name, self.age)
}
}
fn main() {
let ada = User { name: "Ada".to_string(), age: 36, admin: false };
// Like { ...ada, admin: true } in JavaScript.
let promoted = User { admin: true, ..ada.clone() };
println!("{}", ada.greeting());
println!("{:?}", promoted);
}

Output:

Hi, I'm Ada (36)
User { name: "Ada", age: 36, admin: true }
▶ Try it Yourself

A few things are different. Every field has an exact type: age is a u32 (a whole number that can’t be negative), not a general number. #[derive(Debug)] is what lets you print the whole struct with {:?}, like console.log(obj). And why .clone()? The spread ..ada would move the name string out of ada, and then we couldn’t use ada anymore. Cloning first keeps ada intact. That’s ownership at work, and you’ll see it a lot.

Side by side: a discriminated union becomes an enum

Section titled “Side by side: a discriminated union becomes an enum”

TypeScript developers love discriminated unions: a kind field tells you which shape you have, and switch narrows the type:

type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.PI * s.radius ** 2;
case "rect":
return s.width * s.height;
}
}
const shapes: Shape[] = [
{ kind: "circle", radius: 1 },
{ kind: "rect", width: 2, height: 3 },
];
for (const s of shapes) console.log(area(s).toFixed(2));

Rust has this built in, and it’s one of the language’s best features. Each enum variant carries its own data, and match takes it apart:

enum Shape {
Circle { radius: f64 },
Rect { width: f64, height: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rect { width, height } => width * height,
}
}
fn main() {
let shapes = vec![
Shape::Circle { radius: 1.0 },
Shape::Rect { width: 2.0, height: 3.0 },
];
for shape in &shapes {
println!("{:.2}", area(shape));
}
}

Output:

3.14
6.00
▶ Try it Yourself

No kind string is needed: the variant itself is the tag. And match must cover every variant. If you add Triangle later, every match that forgets it becomes a compile error, which is like TypeScript’s never exhaustiveness trick, but always on. More on this in Pattern Matching.

Side by side: undefined and null become Option

Section titled “Side by side: undefined and null become Option”

In TypeScript, optional values show up as undefined, null, or an optional property, and you use ?. and ?? to deal with them:

interface User {
name: string;
email?: string;
}
function findUser(id: number): User | undefined {
if (id === 1) return { name: "Ada", email: "ada@example.com" };
if (id === 2) return { name: "Grace" };
return undefined;
}
for (const id of [1, 2, 3]) {
console.log(`${id}: ${findUser(id)?.email?.toUpperCase() ?? "no email"}`);
}

In Rust, a value that might be missing has the type Option<T>. It’s either Some(value) or None, and you can’t use it as a plain value until you’ve handled the None case:

struct User {
name: String,
email: Option<String>,
}
fn find_user(id: u32) -> Option<User> {
match id {
1 => Some(User { name: "Ada".to_string(), email: Some("ada@example.com".to_string()) }),
2 => Some(User { name: "Grace".to_string(), email: None }),
_ => None,
}
}
// Inside a function that returns Option, `?` works like `?.`:
// if the value is None, stop here and return None.
fn email_domain(id: u32) -> Option<String> {
let email = find_user(id)?.email?;
let at = email.find('@')?;
Some(email[at + 1..].to_string())
}
fn main() {
for id in [1, 2, 3] {
// Like findUser(id)?.email?.toUpperCase() ?? "no email"
let email = find_user(id)
.and_then(|user| user.email)
.map(|email| email.to_uppercase())
.unwrap_or("no email".to_string());
println!("{id}: {email}");
}
println!("{:?}", email_domain(1));
println!("{:?}", email_domain(2));
if let Some(user) = find_user(2) {
println!("found {}", user.name);
}
}

Output:

1: ADA@EXAMPLE.COM
2: no email
3: no email
Some("example.com")
None
found Grace
▶ Try it Yourself

There is no “cannot read properties of undefined” in Rust, because there is no undefined to read from. The compiler simply won’t let you call .to_uppercase() on an Option<String>. And there’s no difference between null and undefined to remember: there’s only None. Learn the helper methods on the Option page, and the short forms on if let.

Side by side: try/catch becomes Result and ?

Section titled “Side by side: try/catch becomes Result and ?”

In JavaScript, a function throws, and you catch the error somewhere up the call stack. Nothing in the function’s signature tells you it might throw:

function parseAge(input) {
const age = Number(input);
if (!Number.isInteger(age)) throw new Error(`'${input}' is not a whole number`);
if (age > 150) throw new Error(`${age} is too old`);
return age;
}
function totalAge(a, b) {
return parseAge(a) + parseAge(b);
}
for (const [a, b] of [["36", "41"], ["36", "abc"], ["36", "200"]]) {
try {
console.log(`Total: ${totalAge(a, b)}`);
} catch (e) {
console.log(`Error: ${e.message}`);
}
}

In Rust, a function that can fail returns a Result: either Ok(value) or Err(error). The ? operator means “if this failed, return the error to my caller right now”. It does the job of letting an exception bubble up, but you can see it in the code:

fn parse_age(input: &str) -> Result<u32, String> {
let age: u32 = input
.parse()
.map_err(|_| format!("'{input}' is not a whole number"))?;
if age > 150 {
return Err(format!("{age} is too old"));
}
Ok(age)
}
fn total_age(a: &str, b: &str) -> Result<u32, String> {
// Each `?` passes an error up to our caller, like an exception would.
let first = parse_age(a)?;
let second = parse_age(b)?;
Ok(first + second)
}
fn main() {
for (a, b) in [("36", "41"), ("36", "abc"), ("36", "200")] {
match total_age(a, b) {
Ok(total) => println!("Total: {total}"),
Err(e) => println!("Error: {e}"),
}
}
}

Output:

Total: 77
Error: 'abc' is not a whole number
Error: 200 is too old
▶ Try it Yourself

The signature -> Result<u32, String> tells every caller “this can fail”, and the compiler won’t let them ignore it. The same goes for async code: where you’d get a rejected Promise in JavaScript, an async Rust function returns a Result, and you write .await?. For your own error types, see Custom Errors. In applications, the anyhow crate gives you something close to “catch any error” (see anyhow & thiserror).

Side by side: array methods become iterators

Section titled “Side by side: array methods become iterators”

The array methods you use every day have direct Rust equivalents:

const orders = [
{ customer: "Ada", total: 120, paid: true },
{ customer: "Grace", total: 80, paid: false },
{ customer: "Linus", total: 200, paid: true },
];
const paidNames = orders.filter((o) => o.paid).map((o) => o.customer);
const revenue = orders.filter((o) => o.paid).reduce((sum, o) => sum + o.total, 0);
const firstBig = orders.find((o) => o.total > 150);
console.log(paidNames.join(", "));
console.log(`Revenue: ${revenue}`);
console.log(`First big order: ${firstBig?.customer ?? "none"}`);
console.log(`Anything unpaid? ${orders.some((o) => !o.paid)}`);

The Rust version uses iterators and closures. The closure syntax is |o| o.paid instead of (o) => o.paid:

struct Order {
customer: String,
total: u32,
paid: bool,
}
fn main() {
let orders = vec![
Order { customer: "Ada".to_string(), total: 120, paid: true },
Order { customer: "Grace".to_string(), total: 80, paid: false },
Order { customer: "Linus".to_string(), total: 200, paid: true },
];
// filter + map, then collect() turns the result into a Vec.
let paid_names: Vec<&str> = orders
.iter()
.filter(|o| o.paid)
.map(|o| o.customer.as_str())
.collect();
// reduce with a starting value is called fold.
let revenue = orders
.iter()
.filter(|o| o.paid)
.fold(0, |sum, o| sum + o.total);
// find returns an Option, because there might be no match.
let first_big = orders.iter().find(|o| o.total > 150);
println!("{}", paid_names.join(", "));
println!("Revenue: {revenue}");
match first_big {
Some(o) => println!("First big order: {}", o.customer),
None => println!("First big order: none"),
}
// some() is called any().
println!("Anything unpaid? {}", orders.iter().any(|o| !o.paid));
}

Output:

Ada, Linus
Revenue: 320
First big order: Linus
Anything unpaid? true
▶ Try it Yourself

There’s one big difference from JavaScript. Each array method in JS builds a whole new array right away. Rust iterators are lazy: filter and map do nothing on their own, and the work happens in one pass when you call something like collect(), sum(), fold(), or a for loop. No temporary arrays are created in between. For the sum alone, you could also write .map(|o| o.total).sum::<u32>(). Sorting lives on Vec (sort, sort_by), not on iterators; see Sorting.

Side by side: Promises become futures + tokio

Section titled “Side by side: Promises become futures + tokio”

Here is some async JavaScript. Watch when "fetching 1" is printed:

async function fetchUser(id) {
console.log(`fetching ${id}`);
await new Promise((resolve) => setTimeout(resolve, 100));
return `user ${id}`;
}
const promise = fetchUser(1); // prints "fetching 1" right away
console.log("promise created");
console.log(await promise);
const [a, b] = await Promise.all([fetchUser(2), fetchUser(3)]);
console.log(`${a}, ${b}`);

In JavaScript, calling an async function starts it immediately. Rust is different: calling an async fn only creates a future, a description of work. Nothing runs until you .await it. And Rust has no built-in event loop, so you add a runtime: tokio is the most popular one.

use std::time::Duration;
async fn fetch_user(id: u32) -> String {
println!("fetching {id}");
tokio::time::sleep(Duration::from_millis(100)).await;
format!("user {id}")
}
#[tokio::main]
async fn main() {
// Creating a future does NOT start it. Nothing is printed yet.
let future = fetch_user(1);
println!("future created");
println!("{}", future.await);
// Like Promise.all: run both at the same time and wait for both.
let (a, b) = tokio::join!(fetch_user(2), fetch_user(3));
println!("{a}, {b}");
}

Output:

future created
fetching 1
user 1
fetching 2
fetching 3
user 2, user 3
▶ Try it Yourself

See the order? In JavaScript, “fetching 1” would come before “promise created”. In Rust, it comes after “future created”, because the future only starts at .await. #[tokio::main] sets up the runtime (your event loop) and runs main on it. tokio::join! waits for both futures together, like Promise.all; tokio::spawn starts a task in the background, which is the closest thing to calling an async function without awaiting it. The Async/Await and Async in Depth lessons explain the details.

Rust in your JavaScript project: WebAssembly and Node addons

Section titled “Rust in your JavaScript project: WebAssembly and Node addons”

You don’t have to rewrite anything to start using Rust. Two common ways to mix it into a JavaScript project:

WebAssembly in the browser. Mark Rust functions with #[wasm_bindgen], build with wasm-pack, and import the result like any ES module. wasm-pack even writes a .d.ts file, so TypeScript knows your function types:

use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn slugify(title: &str) -> String {
title
.to_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join("-")
}
import init, { slugify } from "./pkg/my_lib.js";
await init(); // download and start the .wasm file
console.log(slugify("Hello Rust World")); // "hello-rust-world"

The WebAssembly lesson walks you through the whole setup, and explains which values cross the boundary cheaply. A tip: Wasm pays off for heavy computation (image processing, parsing, simulations), not for small functions you call thousands of times, because every call between JS and Wasm has a small cost.

Native Node.js addons. With napi-rs (the napi and napi-derive crates) you mark a Rust function with #[napi], and it becomes a function you can require or import from Node. Many popular npm packages ship prebuilt Rust binaries this way, so users never need a Rust compiler.

Things that will trip you up as a JavaScript developer

Section titled “Things that will trip you up as a JavaScript developer”

Everyone coming from JavaScript hits the same bumps. Knowing them in advance helps.

Passing a value to a function moves it. In JavaScript, passing an array to a function passes a reference, and you can keep using the array afterwards. In Rust, passing a Vec or String by value moves ownership into the function, and the original variable can’t be used anymore:

fn print_all(names: Vec<String>) {
for name in names {
println!("{name}");
}
}
fn main() {
let names = vec![String::from("Ada"), String::from("Grace")];
print_all(names);
println!("{} names", names.len());
}
error[E0382]: borrow of moved value: `names`
--> src/main.rs:10:26
|
8 | let names = vec![String::from("Ada"), String::from("Grace")];
| ----- move occurs because `names` has type `Vec<String>`, which does not implement the `Copy` trait
9 | print_all(names);
| ----- value moved here
10 | println!("{} names", names.len());
| ^^^^^ value borrowed here after move

The usual fix is to borrow instead: make the function take names: &[String] and call it with print_all(&names). Read Ownership and Borrowing first; they explain this rule, and you’ll meet it every day.

Numbers have types, and they don’t mix. JavaScript has one number. In Rust, 3 is an integer and 2.5 is a float, and you can’t combine them without a conversion:

fn main() {
let items = 3;
let price = 2.5;
let total = items * price;
println!("{total}");
}
error[E0277]: cannot multiply `{integer}` by `{float}`
--> src/main.rs:4:23
|
4 | let total = items * price;
| ^ no implementation for `{integer} * {float}`

Write items as f64 * price, or make items a float from the start (3.0). Also note that integer division drops the remainder: 7 / 2 is 3, not 3.5. See Data Types and Type Casting.

A few more you’ll run into:

  • Futures are lazy and need a runtime. As you saw above, nothing happens until .await, and you need tokio (or another runtime) to run async code at all. Forgetting .await gives you a compiler warning, not a running task.
  • Strings can’t be indexed. name[0] doesn’t compile, because a Rust String is UTF-8 and one character can take several bytes. Use name.chars().next() or .chars().nth(i). And learn the difference between String and &str early: functions usually take &str, structs usually store String. See Strings.
  • No structural typing and no any. In TypeScript, any object with the right fields fits an interface. In Rust, a function that takes a User only accepts a User. For shared behavior, use traits. For JSON whose shape you don’t know, use serde_json::Value.
  • Shared, mutable object graphs are hard. In JavaScript it’s normal to pass one object around and let everyone change it, or to have objects point back to their parent. The borrow checker allows many readers or one writer at a time, never both. Patterns like that need smart pointers (Rc<RefCell<T>>, Arc<Mutex<T>>) or, better, a different design. Expect a few frustrating weeks. It gets much easier.
  • HashMap has no order. JavaScript Maps and objects keep insertion order, so code often relies on it without noticing. A Rust HashMap doesn’t. Sort before printing, or use BTreeMap or the indexmap crate.
  • No exceptions. Errors are values, and you’ll write ? a lot. Calling .unwrap() is fine while learning, but it’s the Rust version of TypeScript’s !: “trust me, it’s there”. If you’re wrong, the program panics.
  • Compile times. There’s no instant reload like node app.js. A first build with many crates takes a while; later builds are incremental and faster. Use cargo check while you edit, since it skips code generation.

You don’t need to read this site in order. As a JavaScript or TypeScript developer, this route skips what you already know and spends time where Rust is different:

  1. Get Started and Rust Tools & Setup: install Rust, set up VS Code with rust-analyzer, run cargo new.
  2. Variables and Data Types: read these properly. The number types are new to you.
  3. Ownership, then Borrowing, then Slices: the most important pages for you. Take your time.
  4. Strings: String vs &str.
  5. Structs and Enums, then match and Pattern Matching: your interfaces and discriminated unions.
  6. Option and Result, then Custom Errors.
  7. Closures and Iterators: your array methods.
  8. Vectors, HashMaps, and HashSet.
  9. Traits, Generics, and Common Traits.
  10. Modules, Cargo & Crates, and Testing.
  11. Lifetimes and Smart Pointers.
  12. Async/Await and Async in Depth.
  13. Serde & JSON, then build the Web API project, or try WebAssembly.

Both exercises give you a JavaScript or TypeScript snippet. Port it to Rust.

This JavaScript counts how often each word appears, then prints the words from most to least common (ties in alphabetical order). Port it to Rust. Hint: use a HashMap<&str, u32> and *counts.entry(word).or_insert(0) += 1. To sort, collect the map into a Vec of (word, count) pairs and use sort_by.

const text = "the cat and the hat and the bat";
const counts = new Map();
for (const word of text.split(" ")) {
counts.set(word, (counts.get(word) ?? 0) + 1);
}
const sorted = [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
for (const [word, n] of sorted) console.log(`${word}: ${n}`);
Show solution
use std::collections::HashMap;
fn main() {
let text = "the cat and the hat and the bat";
let mut counts: HashMap<&str, u32> = HashMap::new();
for word in text.split(' ') {
*counts.entry(word).or_insert(0) += 1;
}
// Like [...counts]: turn the map into a list of (word, count) pairs.
let mut sorted: Vec<(&str, u32)> = counts.into_iter().collect();
// Most common first; ties in alphabetical order.
sorted.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (word, n) in sorted {
println!("{word}: {n}");
}
}

Output:

the: 3
and: 2
bat: 1
cat: 1
hat: 1
▶ Try it Yourself

entry(word).or_insert(0) does the counts.get(word) ?? 0 part and the set in one step: it gives you a mutable reference to the count, creating it with 0 if it’s missing. The sorting is essential here, not just nice to have: without it, a HashMap prints in a different order from run to run.

This TypeScript function takes an options object where every field is optional and fills in defaults with ??. Port it to Rust. Hint: make a struct with Option fields and add #[derive(Default)]. Then ServerOptions { port: Some(3000), ..Default::default() } works like passing { port: 3000 }. For a bool, the default is false.

interface ServerOptions {
host?: string;
port?: number;
verbose?: boolean;
}
function describe(opts: ServerOptions): string {
const host = opts.host ?? "localhost";
const port = opts.port ?? 8080;
const mode = opts.verbose ? "verbose" : "quiet";
return `${host}:${port} (${mode})`;
}
console.log(describe({}));
console.log(describe({ port: 3000 }));
console.log(describe({ host: "example.com", verbose: true }));
Show solution
#[derive(Default)]
struct ServerOptions {
host: Option<String>,
port: Option<u16>,
verbose: bool,
}
fn describe(opts: &ServerOptions) -> String {
let host = opts.host.as_deref().unwrap_or("localhost");
let port = opts.port.unwrap_or(8080);
let mode = if opts.verbose { "verbose" } else { "quiet" };
format!("{host}:{port} ({mode})")
}
fn main() {
println!("{}", describe(&ServerOptions::default()));
println!("{}", describe(&ServerOptions { port: Some(3000), ..Default::default() }));
println!(
"{}",
describe(&ServerOptions {
host: Some("example.com".to_string()),
verbose: true,
..Default::default()
})
);
}

Output:

localhost:8080 (quiet)
localhost:3000 (quiet)
example.com:8080 (verbose)
▶ Try it Yourself

unwrap_or is your ??. as_deref() turns an Option<String> into an Option<&str>, so it can fall back to the plain text "localhost" without copying anything. And ..Default::default() is the Rust way to say “every field I didn’t mention gets its default”, which is exactly what leaving a property out does in JavaScript. A u16 port also means a port like 70000 can’t even be stored by mistake.

  • Much of JavaScript and TypeScript carries over: closures, async/await, destructuring, and array-style methods like map, filter, and find all have close Rust equivalents.
  • The big shifts are ownership instead of a garbage collector, Option instead of undefined and null, and Result + ? instead of throw and try/catch.
  • TypeScript interfaces become structs (for data) or traits (for behavior), and discriminated unions become enums with match.
  • Rust futures are lazy and need a runtime like tokio; tokio::join! plays the role of Promise.all.
  • Cargo replaces npm, package.json, and your scripts; clippy and rustfmt replace ESLint and Prettier; testing is built in.
  • For Express look at axum or actix-web, for fetch look at reqwest, for Prisma look at SeaORM or Diesel, and for Electron look at Tauri. You can also bring Rust into a JS project through WebAssembly or napi-rs.

Quick check

1. What does Rust use instead of undefined and null?

2. In JavaScript, calling an async function starts it right away. What happens when you call an async fn in Rust without .await?

3. Your TypeScript code uses a discriminated union with a kind field. What is the closest Rust feature?

4. Your team's backend uses Express. Which Rust crate is the closest match?

Score: 0 / 4