The file exists, but the compiler can't find it
You split your main.rs into separate files to keep things organized. You create src/game.rs and add a function. Back in main.rs, you write use game::start_game; and hit run. The compiler immediately rejects you with E0432: unresolved import game::start_game. You check the spelling. You check the file name. The file is right there in the directory. The compiler seems blind.
The compiler isn't blind. It's following a map you haven't finished drawing. Rust separates the physical file system from the logical module tree. E0432 means the path you asked for doesn't exist in that tree, or the item you want is hidden behind a locked door. Fixing this error requires understanding how Rust builds its module tree, how visibility propagates, and how paths resolve.
The module tree versus the file tree
Rust does not automatically import every file it finds on disk. The compiler only knows about modules that you explicitly declare. The file system is a storage mechanism. The module tree is the namespace structure the compiler uses to resolve names.
Think of the module tree like a building directory. The file system is the physical layout of rooms. You can build a room anywhere, but if you don't add it to the directory, no one can find it. In Rust, the mod keyword is the entry in the directory.
When you write mod game; in main.rs, you are telling the compiler: "Create a node named game in the module tree, and look for the source code in game.rs or game/mod.rs." Without that declaration, game.rs is just a text file. The compiler ignores it. The import fails because the node game doesn't exist in the tree.
// src/main.rs
/// Declares the game module and links it to game.rs.
mod game;
fn main() {
// This works because 'mod game;' created the node.
use game::start_game;
start_game();
}
// src/game.rs
/// Starts the game loop.
pub fn start_game() {
println!("Game started");
}
The mod declaration is the contract. Without it, the file is invisible to the compiler.
Visibility and the chain of access
Declaring a module is only half the battle. Rust enforces strict visibility rules. Items inside a module are private by default. A private item cannot be accessed from outside its parent module.
Visibility is transitive. If you want to access an item through a path, every node in that path must be visible. If any intermediate module is private, the import fails, even if the final item is public. This is the most common cause of E0432 after missing mod declarations.
Consider a structure where a function is public, but the module containing it is private.
// src/lib.rs
/// Network module is private to this crate.
mod network {
/// Connection submodule is also private.
mod connection {
/// This function is public relative to connection.
pub fn connect() {
println!("Connected");
}
}
}
// src/main.rs
// E0432: unresolved import `network::connection::connect`
// The path is blocked because 'network' is private.
use crate::network::connection::connect;
The compiler rejects this with E0432. The error message points to the unresolved import. The root cause is that network is private. You cannot traverse through a private module to reach a public item. You must mark network as pub to open the door.
// src/lib.rs
/// Network module is now public.
pub mod network {
/// Connection submodule must also be public to be reachable.
pub mod connection {
/// This function is public.
pub fn connect() {
println!("Connected");
}
}
}
// src/main.rs
// This resolves correctly. All nodes in the path are visible.
use crate::network::connection::connect;
Visibility is transitive. A public item inside a private module is unreachable.
Paths: absolute, relative, and crate roots
Once the module tree is built and visibility is set, Rust resolves paths. A path is a sequence of names separated by ::. Paths can be absolute or relative.
Absolute paths start from the crate root. They begin with crate:: for items within the current crate, or with an external crate name like std:: or serde::. Relative paths start with self:: (current module), super:: (parent module), or an identifier (child module).
Using crate:: paths is generally safer. Relative paths like super:: break when you move files around during refactoring. Absolute paths stay valid as long as the hierarchy is preserved.
// src/lib.rs
pub mod utils {
pub fn helper() {}
}
// src/main.rs
// Absolute path from crate root. Robust to refactoring.
use crate::utils::helper;
// Relative path going up one level. Fragile if main.rs moves.
// use super::utils::helper; // Only works if main.rs is nested.
When you see E0432, check the path syntax. If you use super:: in a module that is already at the crate root, the path is invalid. If you use crate:: but the module isn't declared in the root, the path is invalid.
External crates and Cargo.toml
Imports that start with a name that isn't crate, super, or self are treated as external crates. If you write use serde::Serialize;, Rust looks for a crate named serde.
If serde is not listed in your Cargo.toml dependencies, the compiler cannot find the crate. You get E0432. The error might look like "unresolved import serde". The fix is to add the dependency.
# Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
Convention aside: always run cargo check after adding a dependency. It resolves the dependency graph and verifies imports without building the full binary, giving you faster feedback.
Re-exports and flattening APIs
Deep module hierarchies create long import paths. Users of your crate might have to write use my_crate::internal::models::user::User;. This is cumbersome. Rust provides pub use to re-export items under a different path. This flattens the API without changing the internal structure.
// src/lib.rs
mod internal {
pub mod models {
pub mod user {
/// Represents a user.
pub struct User {
pub name: String,
}
}
}
}
// Re-export User at the crate root for ergonomic access.
pub use internal::models::user::User;
// src/main.rs
// Users can now import with a short path.
use crate::User;
Use pub use when you want to expose an item under a shorter path. This is standard practice for library crates. It keeps the internal hierarchy organized while providing a clean public API.
Pitfalls and compiler errors
E0432 manifests in several ways. The compiler message usually points to the specific name that failed to resolve.
- Missing
moddeclaration: The file exists, but you forgotmod name;. The compiler says "unresolved importname". Add the declaration. - Private intermediate module: The item is
pub, but a parent module is private. The compiler says "unresolved importparent::child::item". Mark the parentpub. - Typo in path: You wrote
use crate::utls::helper;instead ofutils. The compiler says "unresolved importutls". Check the spelling. - External crate missing: You imported
tokio::mainbut didn't addtokiotoCargo.toml. The compiler says "unresolved importtokio". Add the dependency. - Wrong file discovery: You have
src/game/mod.rsbut wrotemod game;expecting it to findsrc/game.rs. Rust checksgame.rsfirst, thengame/mod.rs. If neither exists, the module is unresolved. Ensure the file structure matches the declaration.
Convention aside: Rust 2018 and later prefer src/foo.rs over src/foo/mod.rs for single-file modules. The compiler supports both, but the community convention is to use foo.rs unless the module has submodules. Stick to foo.rs for simplicity.
Decision matrix: resolving E0432
Use mod name; when the file exists on disk but the compiler reports the module as unresolved. This links the file to the module tree.
Use pub when the item or module must be accessible from outside its parent. This unlocks the door for imports.
Use pub(crate) when the item is shared within the crate but should be hidden from external users. This provides visibility without exposing the API.
Use pub use when you want to re-export an item to flatten the import path for users. This improves ergonomics.
Check Cargo.toml when the import references an external crate name. This ensures the dependency is available.
Switch to crate:: paths when relative super:: chains become hard to track during refactoring. This makes imports robust to file moves.
Trust the module tree. If the path fails, the tree is wrong, not the file system.