Rust · Beginner

Rust Syntax and Your First Program

8 min readUpdated September 24, 2026Every example verified

In short: A Rust program is compiled by rustc into a native executable and starts running at fn main(). Statements end with semicolons, braces group blocks, println! is a macro that fills {} placeholders with values, and // starts a comment.

How a Rust program runs

Rust is a compiled language. You write the source in a file ending in .rs, and the compiler rustc translates the whole file into a native executable before anything runs. If the compiler finds a problem, such as a misspelt name or a value of the wrong type, it refuses to produce a program at all and prints an error that names the line. That is the trade you make with Rust: a little more time satisfying the compiler, and in return whole categories of mistakes never reach a running program. Most projects use Cargo, Rust's build tool, but a single file compiles directly with rustc main.rs, which is how every program on this site is checked.

Every executable starts in a function called main. The fn keyword introduces a function, the empty parentheses say it takes no parameters, and the braces enclose its body. Inside, statements end with a semicolon and run from top to bottom. Braces also group the body of an if, a loop or a nested block, and indentation is purely for the reader: the compiler counts braces, not spaces. The convention is four spaces per level, which is what the rustfmt tool produces.

println! writes a line to standard output. The exclamation mark means it is a macro rather than an ordinary function. That distinction matters for two reasons: a macro can accept any number of arguments, and it can inspect the text you give it while compiling, so a placeholder {} without a matching value is a compile-time error instead of a wrong line of output. Each {} is filled with the next argument; a name inside the braces, such as {price}, pulls a variable from the surrounding scope (supported since Rust 1.58), and {:.2} formats a number with two decimal places. print! does the same without the trailing newline.

Two slashes start a comment that runs to the end of the line; /* ... */ comments out a block. The compiler ignores both. Names are case sensitive, and Rust expects variables and functions in snake_case; it will still compile a name like totalWeight, but it warns you.

Reading input takes a few more words than printing. std::io::stdin() gives a handle to standard input, .lock() takes exclusive, buffered access to it, and .lines() turns it into a sequence of lines, each one with the line break already removed. The .lines() method comes from the BufRead trait, so the file needs use std::io::BufRead; near the top to bring that method into scope. Every exercise here reads its data this way and prints its answer with println!.

Syntax

 Rust · syntax
use std::io::BufRead;             // brings .lines() into scope

// a line comment: ignored by the compiler
fn main() {                       // the program starts here
    let count = 3;                // a statement ends with ;
    println!("{} items", count);  // a macro call: name!( ... )
    let line = std::io::stdin().lock().lines().next().unwrap().unwrap();
    println!("You typed {line}"); // a variable named inside the braces
}

use lines go at the top of the file. Inside main, each statement ends with a semicolon, and {} in a format string is replaced by the next argument. The .next().unwrap().unwrap() chain takes the first line of input; the error handling lesson explains what the two unwrap() calls do.

Printing with placeholders

Compare the plain {} placeholders with the named {price:.2} and the print! call that does not end its line.

 Rust
fn main() {
    // A notice board for a small bakery
    let loaves = 36;
    let price = 3.5;
    println!("Hollow Oak Bakery");
    println!("Loaves on the shelf: {}", loaves);
    println!("Price per loaf: {price:.2}");
    print!("Today's breads: ");
    println!("{}, {} and {}", "rye", "spelt", "seeded");
}

Output

Hollow Oak Bakery
Loaves on the shelf: 36
Price per loaf: 3.50
Today's breads: rye, spelt and seeded

The first three println! calls each end their line. {} takes the next argument in order, so loaves fills the placeholder in the second line. In the third line the placeholder names the variable directly and adds :.2, so the floating-point value 3.5 is printed with two decimal places as 3.50. print! writes Today's breads: and stops, and the following println! continues on the same line, which is why the last line of output was built by two macro calls. The comment on line 2 produces nothing.

Reading one line of input

The program is run with the single input line Rosscarbery.

 Rust
use std::io::BufRead;

fn main() {
    let mut lines = std::io::stdin().lock().lines();
    let town = lines.next().unwrap().unwrap();
    let town = town.trim();
    println!("Next ferry to {}", town);
    println!("Name length: {} characters", town.chars().count());
}

Input given to the program: Rosscarbery

Output

Next ferry to Rosscarbery
Name length: 11 characters

lines() yields one line at a time. next() asks for the first, and the two unwrap() calls say: I expect a line to exist and I expect reading it to succeed; if either is false, stop the program with an error. That is acceptable in a small program where missing input is a genuine bug; the error handling lesson shows the alternatives. trim() removes spaces at either end, and reusing the name town for the trimmed version is ordinary Rust style called shadowing, covered in variables and mutability. Note chars().count() rather than len(): len() returns the number of bytes, which is larger than the number of characters as soon as a name contains a letter such as é.

Blocks, semicolons and an if

Look at the block assigned to total: its last line has no semicolon.

 Rust
fn main() {
    let crates = 4;
    let bottles_per_crate = 12;
    let total = {
        let loose = 5;
        crates * bottles_per_crate + loose
    };
    println!("Bottles to load: {}", total);
    if total > 50 {
        println!("Use the large van");
    }
    println!("Loading plan printed");
}

Output

Bottles to load: 53
Use the large van
Loading plan printed

A pair of braces makes a block, and a block is itself an expression whose value is its final expression, provided that expression has no semicolon. Here the block computes 4 * 12 + 5, and loose exists only inside it. The if that follows needs no parentheses around its condition; the braces around its body are compulsory even for one statement, which removes a classic source of bugs in languages where they are optional. The control flow lesson builds on this.

Common mistakes

  • Calling println without the exclamation mark

    Why it goes wrong: println is a macro, not a function, and the compiler reports error E0423, expected function, found macro println. The ! is part of the name.

    Fix: Write println!(...). The same applies to print!, format! and vec!.

     Rust · fix
    println!("ready");
  • Leaving off a semicolon between statements

    Why it goes wrong: Rust reads the two lines as one expression and reports that it expected ;. Only the last expression of a block may drop its semicolon, and doing so makes it the value of the block.

    Fix: End every statement with ;. When the compiler points at the end of a line, that is usually the line that needs one.

  • Printing a value with {} that has no text form

    Why it goes wrong: {} uses the Display trait, which numbers and strings implement but tuples, arrays and most structs do not; error E0277 says the type doesn't implement std::fmt::Display.

    Fix: Use {:?}, the Debug format, for those values, or print each part separately.

     Rust · fix
    let pair = (3, 4);
    println!("{:?}", pair);
  • Calling .lines() without use std::io::BufRead

    Why it goes wrong: lines() is defined by the BufRead trait, and a trait's methods are only callable when the trait is in scope. The error is E0599, no method named lines found, and the compiler's help text names the missing import.

    Fix: Add use std::io::BufRead; at the top of the file.

     Rust · fix
    use std::io::BufRead;
    
    fn main() {
        for line in std::io::stdin().lock().lines() {
            println!("{}", line.unwrap());
        }
    }

The pieces of a first program

PieceWhat it does
fn main() { }the entry point; execution starts here
;ends a statement
let name = value;binds a name to a value
println!("{}", x)macro that writes one line; {} takes the next argument
{:?}placeholder using the Debug format, for tuples, arrays and structs
//comment to the end of the line
use std::io::BufRead;brings the trait that provides .lines() into scope

Where you use this

Rust's first natural home is the command-line tool: a program that reads a stream of text, transforms it and prints the result, compiled once into a single fast executable with no runtime to install. A tool that reads lines of a log and prints those above a threshold, or converts a list of measurements to another unit, needs nothing more than fn main, a loop over stdin().lock().lines() and println!. Because the compiler checks every format string and every name before the program exists, the executable you copy to a server does not contain a typo waiting to be reached at three in the morning. Every exercise on this site is that same shape in miniature: read standard input, compute, print.

 Rust · in practice
use std::io::BufRead;

fn main() {
    for line in std::io::stdin().lock().lines() {
        println!("> {}", line.unwrap());
    }
}

Key points

  • rustc compiles the whole file to a native executable before anything runs; an error stops the build.
  • Execution starts in fn main(); braces group blocks and semicolons end statements.
  • println! and print! are macros: the ! is part of the name, and {} placeholders are checked at compile time.
  • // starts a comment; names are case sensitive and written in snake_case.
  • Read input with std::io::stdin().lock().lines(), which needs use std::io::BufRead;.
  • A block's last expression, written without a semicolon, is the value of the block.

Try it yourself

The program reads a town name and prints Ferry to <town>. Add a second line that prints Ticket code: followed by the name in upper case, using the to_uppercase() method on the trimmed name.

Your program
use std::io::BufRead;

fn main() {
    let mut lines = std::io::stdin().lock().lines();
    let town = lines.next().unwrap().unwrap();
    println!("Ferry to {}", town.trim());
}
Input the program receives: Sherkin
Expected output: Ferry to Sherkin Ticket code: SHERKIN

Practise this

Open the Rust playground

Frequently asked questions

Why do println and print in Rust end with an exclamation mark?

Because they are macros, not functions. A macro is expanded by the compiler before type checking, which lets println! accept any number of arguments and verify the format string against them at compile time. An ordinary Rust function has a fixed parameter list and could do neither. The ! tells you, and the compiler, that macro rules apply.

Do I need Cargo to run a single Rust file?

No. rustc main.rs compiles one file into an executable named main (or main.exe on Windows) that you run directly. Cargo becomes worthwhile as soon as a project has more than one file or uses external crates: it manages dependencies, builds and tests with one command. The programs in these lessons use only the standard library, so either route works.

Why does reading a line in Rust need lock(), lines(), next() and two unwrap() calls?

Each call does one honest job. stdin() is the shared handle, lock() claims it and adds buffering, lines() splits the stream into lines, and next() fetches one. next() returns an Option because input can end, and inside it is a Result because reading can fail, so two unwrap() calls say you accept a crash in both cases. Longer programs replace unwrap() with proper handling, covered in the error handling lesson.

Progress is stored only in this browser.

How this page was checked. Every program on it was run with rustc 1.94 at build time by the publishing checks, and the output shown is what it printed. Running Rust inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with rustc 1.94 locally.