Hello World and Program Structure

The main() Function

main() is the entry point of a Rust executable. When the program starts, the Rust runtime does its setup and then calls main. When main returns, the program ends and the process reports an exit status to the operating system.

fn main() {
    println!("main returns ()");
}
main returns ()

Rules for main

Rule Detail
Exactly one per executable A binary crate needs one main. A library crate has none
Defined at the crate root It must be at the top level of the crate root file, such as src/main.rs. A main inside a module does not count
Takes no parameters Command-line arguments are read through std::env::args()
Not generic Type parameters are rejected
Not async async fn main is rejected by the compiler
Returns a type that implements Termination (), !, ExitCode, Result<T, E> and similar

The name is fixed. Main, start, or any other name does not work as an entry point.

No Parameters

// ❌ does not compile
fn main(count: i32) {
    println!("{count}");
}
error[E0580]: `main` function has wrong type
 --> m3.rs:2:1
  |
2 | fn main(count: i32) {
  | ^^^^^^^^^^^^^^^^^^^ incorrect number of function parameters
  |
  = note: expected signature `fn()`
             found signature `fn(i32)`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0580`.

Not Generic

// ❌ does not compile
fn main<T>() {
    println!("generic");
}
error[E0131]: `main` function is not allowed to have generic parameters
 --> n3.rs:2:8
  |
2 | fn main<T>() {
  |        ^^^ `main` cannot have generic parameters

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0131`.

Not async

// ❌ does not compile
async fn main() {
    println!("async");
}
error[E0752]: `main` function is not allowed to be `async`
 --> n2.rs:2:1
  |
2 | async fn main() {
  | ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0752`.

Async runtimes such as Tokio work around this with an attribute macro that generates a normal synchronous main and starts the runtime inside it.

Must Be at the Crate Level

A function named main inside a module is an ordinary function. It is not the entry point.

// ❌ does not compile
mod app {
    pub fn main() {
        println!("inside module");
    }
}
error[E0601]: `main` function not found in crate `n4`
 --> n4.rs:6:2
  |
6 | }
  |  ^ the main function must be defined at the crate level (in `n4.rs`)
  |
note: here is a function named `main`
 --> n4.rs:3:5
  |
3 |     pub fn main() {
  |     ^^^^^^^^^^^^^
  = note: you have one or more functions named `main` not defined at the crate level
  = help: consider moving the `main` function definitions

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0601`.

A module can have its own main function next to a crate-level one. The two are separate functions, and only the crate-level one is the entry point.

mod app {
    pub fn main() {
        println!("app::main");
    }
}

fn main() {
    println!("crate main");
    app::main();
}
crate main
app::main

Return Types

main may return any type that implements the std::process::Termination trait. The trait converts the returned value into an exit status. The standard library implements it for these types:

Return type Exit status
() 0
ExitCode The code stored in the ExitCode
Result<T, E> where T: Termination and E: Debug The status of T for Ok. For Err, prints Error: {error:?} to standard error and exits with 1
! The function never returns, so there is no status

Any other type is rejected:

// ❌ does not compile
fn main() -> i32 {
    0
}
error[E0277]: `main` has invalid return type `i32`
 --> m4.rs:2:14
  |
2 | fn main() -> i32 {
  |              ^^^ `main` can only return types that implement `Termination`
  |
  = help: consider using `()`, or a `Result`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.

Unlike C, returning an integer does not set the exit status. Use ExitCode or std::process::exit for that.

Returning ()

Most programs leave out the return type. A function with no return type returns the unit value (). Writing it out is equivalent:

fn main() -> () {
    println!("explicit unit");
}
explicit unit

A program that returns () exits with status 0 when main ends normally.

Returning Result

A main that returns Result<(), E> can use the ? operator, which returns early from main with the error.

use std::num::ParseIntError;

fn main() -> Result<(), ParseIntError> {
    let number: i32 = "42".parse()?;
    println!("parsed {number}");
    let bad: i32 = "forty-two".parse()?;
    println!("never reached {bad}");
    Ok(())
}
parsed 42
Error: ParseIntError { kind: InvalidDigit }

The Err value is printed with its Debug format on standard error, and the exit status is 1:

./m5; echo "exit=$?"
parsed 42
Error: ParseIntError { kind: InvalidDigit }
exit=1

Box<dyn Error> is a common error type for main because it accepts any error:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let _file = File::open("missing.txt")?;
    println!("opened");
    Ok(())
}
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

Returning ExitCode

std::process::ExitCode gives direct control over the exit status. ExitCode::SUCCESS is 0, ExitCode::FAILURE is 1, and ExitCode::from(n) takes a u8.

use std::num::ParseIntError;
use std::process::ExitCode;

fn run() -> Result<i32, ParseIntError> {
    "12x".parse::<i32>()
}

fn main() -> ExitCode {
    match run() {
        Ok(value) => {
            println!("value = {value}");
            ExitCode::SUCCESS
        }
        Err(error) => {
            eprintln!("error: {error}");
            ExitCode::FAILURE
        }
    }
}
error: invalid digit found in string

This form prints a custom message instead of the Debug output that Result produces.

use std::process::ExitCode;

fn main() -> ExitCode {
    println!("exiting with 3");
    ExitCode::from(3)
}
./m6; echo "exit=$?"
exiting with 3
exit=3

Returning !

! is the never type. A main declared with -> ! must never return normally, for example because it ends the process itself.

fn main() -> ! {
    println!("never returns");
    std::process::exit(7)
}
never returns

Custom Return Types

A program can define its own return type by implementing Termination:

use std::process::{ExitCode, Termination};

enum Outcome {
    Passed,
    Failed(u8),
}

impl Termination for Outcome {
    fn report(self) -> ExitCode {
        match self {
            Outcome::Passed => ExitCode::SUCCESS,
            Outcome::Failed(code) => ExitCode::from(code),
        }
    }
}

fn main() -> Outcome {
    println!("checking");
    if std::env::args().count() > 1 {
        Outcome::Passed
    } else {
        Outcome::Failed(4)
    }
}
./t3; echo "exit=$?"
./t3 ok; echo "exit=$?"
checking
exit=4
checking
exit=0

Command-Line Arguments

main has no parameters. Arguments are read from std::env::args(), an iterator of String values. The first item is the program path as it was invoked. The arguments follow it.

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{args:?}");
    println!("program: {}", args[0]);
    println!("count: {}", args.len() - 1);
    for (index, arg) in env::args().skip(1).enumerate() {
        println!("arg {index}: {arg}");
    }
}
./n1 alpha "two words" 3
["./n1", "alpha", "two words", "3"]
program: ./n1
count: 3
arg 0: alpha
arg 1: two words
arg 2: 3

Quoting on the command line groups "two words" into one argument. env::args() panics if an argument is not valid Unicode. env::args_os() returns OsString values and never panics for that reason.

Ways a Program Ends

Cause Exit status Destructors run
main returns () 0 Yes, for the locals of main
main returns ExitCode or Result As described above Yes
std::process::exit(code) code No
A panic in the main thread 101 Yes, when unwinding

Returning From main Runs Destructors

Local values in main are dropped in reverse order of declaration when main returns.

struct Guard(&'static str);

impl Drop for Guard {
    fn drop(&mut self) {
        println!("dropping {}", self.0);
    }
}

fn main() {
    let _a = Guard("a");
    let _b = Guard("b");
    println!("end of main");
}
end of main
dropping b
dropping a

process::exit Skips Destructors

std::process::exit ends the process immediately with the given status. Values still alive in the calling function are not dropped.

struct Guard(&'static str);

impl Drop for Guard {
    fn drop(&mut self) {
        println!("dropping {}", self.0);
    }
}

fn main() {
    let _a = Guard("a");
    println!("calling exit");
    std::process::exit(0);
}
calling exit

The line dropping a never appears.

A Panic Ends the Program With Status 101

A panic in the main thread unwinds the stack, prints a message to standard error, and exits with status 101.

fn main() {
    println!("before");
    panic!("something went wrong");
}
before

thread 'main' (361873) panicked at m8.rs:3:5:
something went wrong
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
./m8; echo "exit=$?"
exit=101

The number in parentheses is the operating-system thread id and differs on every run.

Other Threads Do Not Keep the Program Alive

The program ends when main returns, even if other threads are still running.

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        thread::sleep(Duration::from_millis(500));
        println!("background finished");
    });
    println!("main finished");
}
main finished

background finished is never printed, because the process ends before the thread wakes up.

Multiple Executables in One Package

A Cargo package can hold several binaries. Each binary crate has its own main:

my_package/
├── Cargo.toml
└── src/
    ├── main.rs          <- main for the default binary
    ├── lib.rs           <- library crate, no main
    └── bin/
        ├── tool_one.rs  <- main for the binary "tool_one"
        └── tool_two.rs  <- main for the binary "tool_two"

cargo run runs the default binary. cargo run --bin tool_one selects another.