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.