Your first Rust program
A Rust program is a set of functions, and one of them is called main. The smallest useful program prints a line, and every part of it is worth knowing before the course adds ownership and types: the entry point, the macro, the literal, the semicolon.
This lesson writes that program twice, once with rustc and once with cargo, then breaks it in two ordinary ways. The point is not the greeting. The point is that the compiler, the shell and the exit status are all part of the program, and each of them can be read.
The program line by line
fn main() declares a function named main with no parameters and no return type; a function that returns nothing returns the unit type, (). The braces hold the body, and the operating system calls this function when the process starts.
println! is a macro. The exclamation mark is not decoration: the macro expands at compile time into the code that formats the arguments and writes them to standard output, and it adds a newline. The argument here is a string literal, and the semicolon ends the statement.
fn main() {
println!("Hello, Rust");
}
rustc --edition 2024 hello.rs
./hello
echo "status: $?"
Hello, Rust
The status is zero because main reached its closing brace, which is the definition of success for a process. Nothing was printed by the compiler, which means no warnings; the next lesson reads the ones that do appear.
The exit status
A process reports one number to the shell, from 0 to 255. Zero means success, and anything else is a failure whose meaning the program decides. Rust can end a program early with std::process::exit, which takes the number and does not return.
The program below prints one line and exits with 3. The shell shows that number with $?, and a script can branch on it. A function that returns ExitCode from main does the same job without a call, which the lesson on functions shows.
fn main() {
println!("before the exit");
std::process::exit(3);
}
rustc --edition 2024 exit_code.rs
./exit_code
echo "status: $?"
before the exit
status: 3
Everything before the exit was flushed to the terminal, because standard output to a terminal is line buffered. Redirected to a file, the buffer would be flushed at exit as well, which is one reason a panic and an exit can interleave output in surprising ways.
A missing semicolon
Statements end with a semicolon, and expressions do not. Removing the semicolon after let x = 5 does not make the code slightly wrong; it makes the next line part of the same expression, and the parser cannot continue.
The message is short and points at both places: the position where the semicolon should be, and the token that turned up instead. Parser errors carry no error code, unlike the type errors later in the course, because they are reported before the compiler knows what the types are.
fn main() {
let x = 5
println!("{}", x);
}
rustc --edition 2024 semicolon.rs
error: expected `;`, found `println`
--> semicolon.rs:2:14
|
2 | let x = 5
| ^ help: add `;` here
3 | println!("{}", x);
| ------- unexpected token
error: aborting due to 1 previous error
The suggestion is literal: the compiler inserts the character for you in the message. In an editor with rust-analyzer the same mistake appears as soon as the line is typed, which is the reason the previous lesson put an editor in the loop.
The same program with cargo
cargo new creates a package: a manifest and a source file, and a .gitignore and a git repository unless you pass --vcs none. The manifest names the package, its version and its edition, and it is the file every later lesson edits when a dependency or a build setting is needed.
cargo run builds and runs in one step, and the output shows the three phases: compiling, finishing, running. The executable lands in target/debug, a directory that is never part of the sources and is recreated by a build.
hello_cargo/Cargo.lock
hello_cargo/Cargo.toml
hello_cargo/src/main.rs
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"
[dependencies]
cargo new --vcs none hello_cargo
cd hello_cargo
cargo run
Compiling hello_cargo v0.1.0 (/tmp/rw/L004/hello_cargo)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.62s
Running `target/debug/hello_cargo`
Hello, world!
The greeting comes from the template, and the first line tells you the package name and version that cargo read from the manifest. Add --quiet to hide cargo's own lines and keep only the output of the program, which is what a script usually wants.
print, println and placeholders
print! writes without a newline and println! writes one. An empty println!() writes a bare newline, which is how two calls to print! are closed. Braces are placeholders, filled from the arguments in order.
Braces with a name inside, {name}, read a variable or an argument by name, and {:?} asks for the debug representation instead of the display one. The last line also shows width and alignment: {:>5} right-aligns in five columns and {:^5} centres. The lesson on formatting covers the whole specification.
fn main() {
print!("one ");
print!("two");
println!();
println!("sum: {}, product: {}", 2 + 3, 2 * 3);
println!("{:?}", (1, "two", '3'));
println!("{:>5}|{:<5}|{:^5}", "ab", "cd", "ef");
}
rustc --edition 2024 printing.rs
./printing
one two
sum: 5, product: 6
(1, "two", '3')
ab|cd | ef
The tuple line shows the debug form: parentheses, commas and quotes, which is enough to see what a value contains. The last line has no separator between the fields on purpose; the bar is the third character of each field, so the alignment is visible.
Exercises
- Make hello.rs print its own name and the edition it was compiled with, using env!("CARGO_PKG_NAME") in a cargo project and a plain literal with rustc.
- Change the exit code to 0 and to 255, run the program and read $? after each. Which numbers does the shell accept?
- Add a third print! between the two in printing.rs and predict the output before you run it.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
println!add thatprint!does not?Answer
A newline after the text
println! ends the line, print! leaves the cursor where the next write starts.
-
What is the exit status of a program whose main reaches its closing brace?
Answer
0
Reaching the end of main is success, and success is zero on every Unix-like system.
-
What does
cargo new --vcs none hello_cargocreate?Answer
A Cargo.toml and a src/main.rs, with no git repository
The template is the manifest and the source; --vcs none skips the repository, and the target folder appears at the first build.
-
What does the semicolon at the end of a statement do?
Answer
It ends the statement and makes the block value the unit type
Without a semicolon the expression becomes the value of the block, which is often a type error.
-
What does this program print?
fn main() { print!("one "); print!("two"); println!(); println!("sum: {}", 2 + 3); }Answer
one two / sum: 5
The two print! calls share a line, the empty println! ends it, and the last line starts a new one.
-
What does this program print?
fn main() { println!("before the exit"); std::process::exit(3); }Answer
before the exit
The program prints one line and exits; the number 3 goes to the shell, not to standard output.
-
What is the first line of the compiler output for this program?
fn main() { let x = 5 println!("{x}"); }Answer
error: expected
;, foundprintlnThe parser wants the semicolon at the end of the let statement and reports the token that followed.
-
Which cargo command builds the package and then runs the binary?
Answer
cargo run
cargo run compiles when needed and then starts the program, which is the three lines cargo prints.
-
What is the first line of the compiler output for this program?
fn main() { let x = println!("hi"); println!("{x}"); }Answer
error[E0277]:
()doesn't implementstd::fmt::Displayprintln! returns the unit type, and the unit type cannot be printed with the display placeholder.
-
What does this program print?
fn main() { println!("{:?}", (1, "two")); }Answer
(1, "two")
The debug form of a tuple keeps the parentheses, the comma and the quotes around the string.
The code on this page was built with rustc 1.96.1 and cargo 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev