Skip to content

Hello World in Rust

12 min read

Your first Rust program! We’ll create a classic “Hello World” and compare it to TypeScript/JavaScript.


Rust programs start with a main function, just like many other languages. Unlike JavaScript, which is interpreted, Rust is compiled to a native executable.

Time required: 15-20 minutes


Let’s start with something familiar:

hello.ts
function main() {
console.log("Hello, world!");
}
main();

Running it:

Terminal window
# Option 1: With ts-node
ts-node hello.ts
# Option 2: Compile then run
tsc hello.ts # Creates hello.js
node hello.js # Runs the JS file

What happens:

  1. TypeScript is transpiled to JavaScript
  2. Node.js interprets the JavaScript
  3. V8 JIT-compiles hot code paths
  4. Output appears in terminal

hello.rs
fn main() {
println!("Hello, world!");
}

Running it:

Terminal window
# Compile
rustc hello.rs # Creates executable 'hello'
# Run
./hello # macOS/Linux
hello.exe # Windows

What happens:

  1. Rust is compiled to native machine code
  2. Linker creates standalone executable
  3. No runtime or interpreter needed
  4. Output appears in terminal

TypeScript:

function main() {
// Function declaration
console.log("Hello, world!"); // Print to console
}
main(); // Call the function

Rust:

fn main() { // Function declaration
println!("Hello, world!"); // Print to console (with newline)
}
// main() is auto-called (entry point)
AspectTypeScriptRust
Function keywordfunction or =>fn
Print functionconsole.log()println!()
Statement end; optional (ASI; most styles keep it); required
Block style{ same line or next{ same line (idiomatic)
Indentation2 spaces (common)4 spaces (standard)
Entry pointCall main() explicitlymain() called automatically
Macro syntaxNone (no macros)! indicates macro

Notice the ! in println!():

println!("Hello, world!"); // Macro (notice the !)

What’s a macro?

  • Code that writes code at compile time
  • Like a template that expands before compilation
  • println! is a macro, not a function

Why !?

  • The ! simply marks a macro invocation — it has nothing to do with format strings
  • Macros can do things functions can’t (e.g. accept a variable number of arguments and check the format string at compile time)
  • You’ll see it on vec!, format!, assert!, and many others

Compare to TypeScript:

// TypeScript has no macro system
// Closest equivalent: template literals
console.log(`Hello, world!`);

Rust also has print! (without newline):

print!("Hello, "); // No newline
print!("world!\n"); // Explicit newline

Create the file:

Terminal window
# Create hello.rs
echo 'fn main() {
println!("Hello, world!");
}' > hello.rs

Compile and run:

Terminal window
# Compile
rustc hello.rs
# Run
./hello

Pros:

  • Simple and direct
  • No additional files needed
  • Good for learning

Cons:

  • No dependency management
  • No standard project structure
  • Manual compilation

When to use: Quick tests, learning exercises


Create project:

Terminal window
# Create new project
cargo new hello_world
cd hello_world

This creates:

hello_world/
├── Cargo.toml # Project manifest (like package.json)
└── src/
└── main.rs # Your code (already has Hello World!)

Run it:

Terminal window
cargo run

Output:

Compiling hello_world v0.1.0 (/path/to/hello_world)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
Running `target/debug/hello_world`
Hello, world!

Pros:

  • Standard project structure
  • Dependency management built-in
  • Automatic compilation
  • Testing framework included

Cons:

  • Slightly more complex for tiny scripts

When to use: Any real project (always use this!)

Compare to Node.js/TypeScript:

Terminal window
# Node.js equivalent
npm init -y # Create package.json
npm install -D typescript @types/node
npx tsc --init # Create tsconfig.json
# Create src/index.ts
npm run dev # (after configuring package.json)

Cargo is simpler - one command!


Method 3: Cargo Script (nightly-only, unstable)

Section titled “Method 3: Cargo Script (nightly-only, unstable)”

Warning: Single-file “cargo scripts” are an unstable feature. As of Rust 1.96.0 they only work on the nightly toolchain behind the -Zscript flag. On stable, cargo rejects an embedded manifest with error: embedded manifest ... requires -Zscript.

When enabled, a script carries its dependencies in a --- TOML frontmatter block at the top of the file (this is the real syntax — there is no // cargo.toml comment manifest):

#!/usr/bin/env -S cargo +nightly -Zscript
---
[package]
edition = "2024"
---
fn main() {
println!("Hello, world!");
}

Run directly:

Terminal window
chmod +x hello.rs
./hello.rs

When to use: Quick automation scripts — but only once you are on nightly and accept that the format may still change before it stabilizes. For anything real, prefer Method 2 (a Cargo project).


Key Differences from TypeScript/JavaScript

Section titled “Key Differences from TypeScript/JavaScript”

TypeScript:

Terminal window
tsc hello.ts # Transpiles to hello.js
node hello.js # Node.js interprets
# Size: hello.js is ~same size as hello.ts

Rust:

Terminal window
rustc hello.rs # Compiles to machine code
# Size: executable is larger (~400KB for "Hello World")
# But runs much faster and needs no runtime

Why Rust binary is larger:

  • Includes all dependencies statically
  • No runtime needed
  • Can be stripped for production

Strip symbols for smaller binary:

Terminal window
strip hello # Removes debug symbols (roughly a 20-30% cut for a tiny binary)

TypeScript/Node.js:

Terminal window
time node hello.js
# real 0m0.050s (50ms)

Rust:

Terminal window
time ./hello
# real 0m0.001s (1ms)

Why: Rust is native code, no runtime or interpreter startup.

TypeScript:

// Must explicitly call main
function main() {
console.log("Hello!");
}
main(); // ← Required!

Rust:

// main() is automatically called at program start
fn main() {
println!("Hello!");
}
// No explicit call needed

TypeScript/JavaScript:

console.log("Hello"); // No newline is added
console.log("World"); // Separate line
// Output:
// Hello
// World

Rust:

println!("Hello"); // Newline added automatically
println!("World"); // Separate line
print!("Hello"); // No newline
print!(" World\n"); // Manual newline
// Both output:
// Hello
// World

Wrong:

fn main() {
println("Hello, world!"); // Error: expected function, found macro `println`
}

Right:

fn main() {
println!("Hello, world!"); // Correct: println! is a macro
}

Error message:

error[E0423]: expected function, found macro `println`
--> hello.rs:2:5
|
2 | println("Hello, world!");
| ^^^^^^^ not a function
|
help: use `!` to invoke the macro
|
2 | println!("Hello, world!");
| +

Why: println! is a macro, not a function. The ! is required.

A missing ; is only an error when another statement follows it (or when a non-() value would be silently discarded). A single trailing expression with no semicolon is fine — it just becomes the block’s return value.

Compiles and runs (the trailing expression evaluates to ()):

fn main() {
println!("Hello, world!") // OK as the last expression — no `;` needed
}

Error — the moment a second statement follows the un-terminated one:

fn main() {
println!("Hello, world!") // now this needs a `;`
println!("Again!");
}

Error message:

error: expected `;`, found `println`
--> hello.rs:2:30
|
2 | println!("Hello, world!")
| ^ help: add `;` here
3 | println!("Again!");
| ------- unexpected token
error: aborting due to 1 previous error

Why: Unlike JavaScript (which inserts semicolons automatically), Rust uses ; to separate statements. The safe habit is to end every statement with ;; omit it only on a deliberate trailing expression.

Wrong:

Terminal window
# Creating hello.js or hello.txt
echo 'fn main() { ... }' > hello.js
rustc hello.js // Won't work well

Right:

Terminal window
# Use .rs extension
echo 'fn main() { ... }' > hello.rs
rustc hello.rs //

Why: While rustc might compile it, tooling (rust-analyzer, cargo) expects .rs files.

Wrong:

Terminal window
./hello.rs // Cannot execute text file

Right:

Terminal window
# Compile first
rustc hello.rs
# Then run the executable
./hello //

Why: Rust is compiled, not interpreted. You run the compiled binary, not the source file.


Don’t:

Terminal window
rustc my_app.rs
rustc module1.rs
rustc module2.rs
# Manual compilation is painful

Do:

Terminal window
cargo new my_app
cd my_app
cargo run
# Cargo handles everything

During development:

Terminal window
cargo run # Debug build, fast compile

For production:

Terminal window
cargo build --release # Optimized build, slow compile, fast execution
./target/release/hello_world

Compare compile times:

Terminal window
time cargo build # ~0.5s (debug)
time cargo build --release # ~3s (release, but executable is 10x faster)
Terminal window
# Format a single file
rustfmt hello.rs
# Format entire project
cargo fmt
# Check without formatting
cargo fmt -- --check

Rust standard style:

  • 4-space indentation (not 2!)
  • Opening brace on same line
  • No trailing commas in single-line
fn main() {
let x = 42;
println!("The value is: {}", x); // Format with variables
println!("x = {x}"); // Shorter syntax (Rust 2021+)
println!("Multiple: {} and {}", x, 10); // Multiple values
}

Compare to TypeScript:

const x = 42;
console.log("The value is:", x); // Comma-separated
console.log(`The value is: ${x}`); // Template literal

Real-World Example: Slightly More Complex Hello World

Section titled “Real-World Example: Slightly More Complex Hello World”

Let’s make it more interesting:

TypeScript:

function greet(name: string): void {
console.log(`Hello, ${name}!`);
}
function main(): void {
const names = ["Alice", "Bob", "Charlie"];
names.forEach((name) => greet(name));
}
main();

Rust:

fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let names = vec!["Alice", "Bob", "Charlie"];
for name in names {
greet(name);
}
}

Run it:

Terminal window
# Save as greet.rs
rustc greet.rs
./greet

Output:

Hello, Alice!
Hello, Bob!
Hello, Charlie!

Key differences:

  • &str is a string slice (we’ll learn this in section 02)
  • vec![] is a vector (like an array)
  • for loop instead of .forEach()
  • No void return type needed


Create and run a Hello World program using both methods:

Terminal window
# Method 1: rustc
echo 'fn main() { println!("Hello from rustc!"); }' > hello1.rs
rustc hello1.rs
./hello1
# Method 2: cargo
cargo new hello2
cd hello2
cargo run

Change the program to print your name:

fn main() {
println!("Hello, <YOUR_NAME>!");
}

Create a program that prints multiple lines:

fn main() {
println!("Line 1");
println!("Line 2");
println!("Line 3");
}

Use print! to print on the same line:

fn main() {
print!("Hello, ");
print!("world!");
println!(); // Add newline at end
}
fn main() {
let name = "Rustacean";
let age = 5;
println!("My name is {} and I am {} years old", name, age);
}
All Solutions

All solutions are provided in the exercises above! Try modifying them and running with cargo run.


What you’ve learned:

  • Basic Rust syntax (fn main, println!)
  • How to compile with rustc
  • How to create projects with cargo
  • Differences between print! and println!
  • Key differences from TypeScript/JavaScript

Key syntax:

fn main() { // Entry point
println!("Text"); // Print with newline
println!("Value: {}", x); // Print with formatting
}

Commands to remember:

Terminal window
rustc file.rs # Compile single file
cargo new project # Create new project
cargo run # Build and run
cargo fmt # Format code

You’re ready to learn Cargo in depth!