The three keywords that organize your code
You're building a CLI tool. main.rs has grown to 400 lines. You decide to move the argument parsing logic to a separate file. You create args.rs, write the function, and go back to main.rs to call it. The compiler screams E0425 (cannot find value). You didn't just move code. You changed the namespace. Rust requires you to declare the relationship between files explicitly.
You see mod, use, and maybe extern crate in old tutorials. They all look like import statements, but they do completely different jobs. mod declares a module. use creates a shortcut. extern crate links external dependencies, though modern Rust handles that automatically. Understanding the distinction stops the compiler errors and makes your project structure intentional.
Concept in plain words
Rust organizes code into a tree. The root is your crate. Branches are modules. Leaves are items like functions, structs, and traits.
mod tells the compiler where a branch exists. It defines the structure. When you write mod foo, you are saying "there is a module named foo attached to this point in the tree." The compiler then looks for the code that fills that module, either in a block or a file.
use creates a shortcut. It doesn't change the structure. It just lets you refer to an item with a shorter name. If a path is crate::utils::parsers::json::parse, use lets you type parse instead. It's purely for ergonomics.
extern crate used to tell the compiler to link a dependency from another crate. In Rust 2018 and later, the compiler reads Cargo.toml and does this automatically. You rarely need to type extern crate anymore.
Think of a restaurant kitchen. mod is the room layout. You have a pantry, a prep station, and a grill. mod pantry tells the manager there's a pantry. use pantry::flour is like putting a bag of flour on your cutting board so you don't have to walk to the pantry every time you need it. extern crate is like ordering ingredients from a supplier. In the old days, you had to sign a receipt for every supplier. Now, the manager checks the delivery manifest and handles the paperwork automatically.
Minimal example
Here is how the three keywords interact in a single file.
/// Defines a module named `calculator`.
/// This creates a namespace. Items inside are private by default.
mod calculator {
/// Adds two numbers.
/// The `pub` keyword makes this function visible outside the module.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
/// Brings `add` into the current scope.
/// Now we can call `add` directly instead of `calculator::add`.
use calculator::add;
fn main() {
// Call using the shortcut created by `use`.
let sum = add(1, 2);
println!("Sum: {}", sum);
// We can still use the full path if we want.
let product = calculator::add(3, 4);
println!("Product via full path: {}", product);
}
mod declares the namespace. use imports the item. The code compiles and runs. At runtime, there is no mod and no use. The binary is just machine code. These keywords exist only to guide the compiler.
How the compiler resolves paths
When you write mod foo;, the compiler searches for the module's code. It looks for foo.rs or foo/mod.rs relative to the current file. If it finds the file, it parses it and attaches the definitions to the module tree. If the file doesn't exist, you get a compilation error.
Inline modules work differently. You can write mod foo { ... } with the code inside braces. This is useful for small modules or examples, but real projects split code into files. The compiler treats both forms the same once parsed.
use paths resolve relative to the crate root by default. If you write use std::io::Read, the compiler looks for std at the root, then io, then Read. You can use super to go up one level or self to refer to the current module, but starting from the root is the standard pattern. This avoids confusion when you move files around.
extern crate is different. It declares a dependency on a crate outside your project. In Rust 2015, you had to write extern crate serde; to use Serde. In Rust 2018+, the compiler reads Cargo.toml and generates those declarations automatically. You just write use serde::Serialize; and it works.
Think of mod as the blueprint. It tells the compiler where the walls go. Without it, the code is just a pile of bricks.
Realistic example
A realistic project has multiple files. Here is how mod and use work across files.
main.rs:
/// Declares the `utils` module.
/// The compiler looks for `utils.rs` or `utils/mod.rs`.
mod utils;
/// Declares the `models` module.
mod models;
/// Imports `User` from the `models` module.
/// This shortens the path for use in `main`.
use models::User;
fn main() {
// Create a user using the shortcut.
let user = User::new("Alice");
// Call a function from `utils`.
// We didn't import it, so we use the full path.
utils::print_user(&user);
}
utils.rs:
/// Re-exports `print_user` so it's public.
/// `pub` makes the function visible to other modules.
pub fn print_user(user: &crate::models::User) {
println!("User: {}", user.name);
}
models.rs:
/// Defines the `User` struct.
/// `pub` makes it visible outside this module.
pub struct User {
pub name: String,
}
impl User {
/// Creates a new user.
pub fn new(name: &str) -> Self {
User {
name: name.to_string(),
}
}
}
Notice the crate:: prefix in utils.rs. When you reference items across modules, you often need to start from the crate root. crate::models::User is unambiguous. You could also use super::models::User if utils and models are siblings, but crate:: is safer when refactoring.
Convention aside: Put mod declarations at the top of your file, followed by use statements. This keeps the structure clear. Readers expect to see the module tree first, then the imports.
Pitfalls and compiler errors
The module system catches mistakes early. Here are the common errors and how to fix them.
E0603: module is private
You try to use an item, but the compiler says the module or item is private. Rust makes everything private by default. You need pub to expose items.
mod hidden {
// Missing `pub`. This function is private to `hidden`.
fn secret() {}
}
fn main() {
// Error E0603: function `secret` is private.
hidden::secret();
}
Fix it by adding pub.
mod hidden {
pub fn secret() {}
}
Visibility is the gatekeeper. use is just a key. If the gate is locked, the key does nothing.
E0432: unresolved import
You wrote a use statement, but the path is wrong. The compiler can't find the item.
use std::io::NonExistent;
fn main() {}
Check the path. Use your editor's autocomplete. Paths are case-sensitive. std::io::Read works. std::io::read does not.
E0425: cannot find value
You moved a function to a file but forgot to declare the module.
// main.rs
fn main() {
// Error E0425: cannot find value `helper`.
helper();
}
// helper.rs exists, but `mod helper;` is missing in main.rs.
Add mod helper; to main.rs. The compiler needs to know the file exists.
Don't fight visibility. Make things pub when they need to be shared. Keep things private when they are internal details. The compiler enforces this boundary for a reason.
Decision: when to use what
Use mod when you are defining a new namespace or declaring a module file to the compiler. Use mod to organize code into logical groups. Use mod to split large files into smaller, manageable pieces.
Use use when you want to shorten a long path or bring an item into the current scope for easier access. Use use to reduce repetition. Use use to clarify intent by importing specific items rather than relying on full paths.
Use extern crate when you are writing a proc-macro crate or a build script that needs to explicitly link a dependency. Use extern crate when you are maintaining legacy code on Rust 2015. Reach for Cargo.toml instead for modern applications; the compiler handles external crates automatically.
Reach for pub use when you want to re-export an item so downstream users can import it from your crate. This is common in library design where you want to present a clean API.