Paths for Referring to Items in the Module Tree

Learn how absolute and relative paths work in Rust’s module system, how to navigate with crate, super, and self, and how paths interact with privacy rules.

To use a function, struct, enum, or any other item that lives in a module, you need a way to tell the compiler exactly where that item is. Rust calls these directions paths. A path is a sequence of one or more identifiers separated by ::, much like a filesystem path separates directory names with /. The final identifier in the path names the item you want, and every preceding identifier must name a module that contains the next piece of the chain.

The same path syntax appears in use statements, function calls, type annotations, and anywhere the compiler needs to resolve a name. This section explains the two kinds of paths—absolute and relative—and the special keywords that make navigation practical.

Absolute Paths: Starting from the Crate Root

An absolute path begins at the top of the current crate and spells out every module boundary down to the target item. You signal an absolute path with the crate keyword, which refers to the root module of the crate you are writing.

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}
pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();
}

The path crate::front_of_house::hosting::add_to_waitlist reads from left to right: start at the crate root, go into module front_of_house, then into its child module hosting, and finally call the function add_to_waitlist. The compiler resolves each segment in order, and every module along the way must be accessible from the point where the path is written.

Absolute paths work the same way inside deeply nested modules; crate always refers to the outermost module. This consistency makes absolute paths a reliable choice when you want to be explicit about where an item lives, regardless of how deep the current module is.

External Crates Use a Different Root:

Absolute paths for items from other crates start with the crate name instead of crate. For example, std::collections::HashMap is an absolute path into the standard library. The crate keyword only refers to the crate you are currently building—it cannot be used to reach into dependencies.

Relative Paths: Starting from the Current Module

A relative path begins in the module where the path is written. You can think of it as dropping the crate:: prefix and letting the compiler figure out the route from the current location.

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}
pub fn eat_at_restaurant() {
    // Relative path — `front_of_house` is in the same module as this function.
    front_of_house::hosting::add_to_waitlist();
}

Here eat_at_restaurant and front_of_house are siblings at the crate root. Because front_of_house is in scope at that location, the relative path front_of_house::hosting::add_to_waitlist resolves correctly. Relative paths save typing and are often clearer when the target module is nearby.

The mental model for a relative path is straightforward: the first segment names something that is directly visible in the current scope—either a child module, an item brought in with use, or the current module itself via self.

self — The Current Module

The self keyword stands for the current module, exactly like . in a filesystem path. It is often used when you want to be explicit about starting a relative path from the enclosing module, or when disambiguation is needed.

mod kitchen {
    pub fn cook() {}
    pub fn serve() {
        // These three calls are equivalent
        cook();                   // relative path (no prefix)
        self::cook();             // explicit self path
        crate::kitchen::cook();   // absolute path
    }
}

self becomes particularly useful in use statements (covered in the next section), but you will also encounter it in paths that import items relative to the current module when a name collision might otherwise confuse the reader or the compiler.

super — The Parent Module

Rust provides super to move one level up the module tree, toward the root. It is the equivalent of .. in a filesystem and gives child modules access to items in their parent.

fn serve_order() {}
mod back_of_house {
    fn fix_incorrect_order() {
        // `serve_order` is in the parent module, not in `back_of_house`.
        super::serve_order();
        cook_order(); // sibling in the same module
    }
    fn cook_order() {}
}

fix_incorrect_order is inside back_of_house, but it needs to call serve_order, which lives one level up. The path super::serve_order() resolves to the serve_order defined at the crate root. Without super, the compiler would look for serve_order inside back_of_house and fail.

You can chain super multiple times to reach grandparent modules and beyond. For example, super::super::some_function moves up two levels before looking for some_function. The only hard limit is the crate root—trying to move above the root with super causes a compile error.

super goes only one level at a time:

It is a common mistake to treat super as a “go to the root” shortcut. super takes you to the immediate parent, not the crate root. If you need the root, use crate::.

How Visibility Gates Every Path

A path that is syntactically valid still has to pass Rust’s privacy checks before the compiler accepts it. Two conditions must both be true:

  1. Every module in the path must be accessible from the location where the path is written. A module is accessible if it is either a parent (or grandparent) of the current module, or if it has been marked pub and all its ancestors outward are also accessible.
  2. The final item—the function, struct, or other thing being reached—must itself be visible at the point of use. Even if every module gate is open, a private item inside the last module makes the path fail.

Think of the path as a series of doors, one for each ::. A door closes if the corresponding module is private and you are trying to enter from the side (a sibling or an unrelated branch). Parents and ancestors are always open to their children—a child module can reach into its parent’s private items without any pub keyword—but siblings cannot see each other’s private contents.

mod a {
    fn private_function() {}
    pub fn public_function() {}
    pub mod b {
        pub fn also_public() {}
    }
    mod c {
        pub fn public_inside_private_module() {}
    }
}
fn main() {
    a::public_function();                 // OK — `a` is accessible as a sibling
    a::b::also_public();                  // OK — `b` is public inside `a`
    // a::private_function();             // ERROR — private function in `a`
    // a::c::public_inside_private_module(); // ERROR — module `c` is private
}

The first failing path illustrates that even though a itself is accessible to main (they are siblings at the crate root), the function private_function is not public. The second failing path fails because module c is private, so it acts as a closed gate—the fact that public_inside_private_module is pub does not matter; the module gate before it blocks access.

Children can always see into their parents, so paths that go upward and then stay in the parent’s scope often succeed where sideways paths fail.

mod parent {
    fn secret() {}
    mod child {
        pub fn access_secret() {
            super::secret(); // OK — child can see parent's private items
        }
    }
}

If your path compiles, visibility rules are satisfied:

When the compiler accepts a path, you know that every gate along the path is open from your current module’s perspective. This guarantee is what makes the module system feel predictable once you internalize the parent‑child accessibility rule.

A Realistic Navigation Example

Larger projects often nest modules several layers deep. The following example combines absolute paths, relative paths, super, and visibility rules in a single crate.

mod kitchen {
    pub mod appliances {
        pub fn preheat_oven(temp: u32) {
            println!("Oven preheating to {}°C", temp);
        }
    }
    mod inventory {
        pub fn check_stock(item: &str) -> bool {
            // Can reach sibling module `appliances` because it is public?
            // No — `appliances` is a sibling, but it IS public.
            // Actually `appliances` is a sibling of `inventory`, but `inventory` is
            // within `kitchen`. Siblings at the same level see each other if public.
            // So this path would compile if `appliances` is pub:
            super::appliances::preheat_oven(180);
            item == "flour"
        }
    }
}
pub fn prepare_dinner() {
    // Absolute path from the crate root
    crate::kitchen::appliances::preheat_oven(200);
    // Relative path from within `prepare_dinner` — `kitchen` is in scope
    kitchen::inventory::check_stock("salt");
}

The call inside inventory::check_stock uses super::appliances::preheat_oven(180). super moves from inventory up to kitchen, and then the path continues into appliances, which is a sibling of inventory. Because appliances is pub, the path is valid. The sibling‑to‑sibling access works because the path goes up to the common parent first—the sibling module itself is a separate branch, and going sideways without moving up is not directly allowed; super provides the necessary upward step.

Forgetting pub on an intermediate module breaks the chain:

In the inventory module, if you tried to write kitchen::appliances::preheat_oven(180) (a relative path from inside inventory), the compiler would reject it because kitchen is not in scope inside inventoryinventory can see kitchen only as a parent, not as a sibling. The correct relative path must use super:: to move up first. This distinction causes confusion even for experienced developers.

Common Mistakes and Misconceptions

Paths seem simple on the surface, but a handful of misunderstandings account for the majority of compile errors beginners encounter.

Assuming a public item inside a private module is reachable. A pub item behind a private module is invisible from outside that module. The gate is the module, not the item. You either make the module public or re‑export the item with pub use (discussed later).

Using super to reach the crate root. super only moves one level up. If you are three levels deep, super takes you to the second level, not the surface. Use crate:: when you need the root.

Treating crate as a magic keyword that works from any file. crate is available only inside the crate you are currently compiling. If you are writing a library and a binary in the same package, each has its own crate root; a library cannot use crate to reach the binary’s code and vice versa.

Mixing up the direction of relative paths. A path that starts with an identifier (like some_module::func()) looks for some_module in the current scope. If the module is a sibling of the current module but is not imported or in the ancestor chain, the path fails. The fix is often to move up with super or to switch to an absolute path.

Trying to chain super beyond the crate root. super cannot go past the root module. The compiler will emit an error like “there is no super of the root module.” In practice this only happens when you are already at the crate root and write super::, which is a logic mistake.

Bringing Paths into Everyday Use

Every call to println!, every construction of a Vec, and every import of a HashMap relies on the same path mechanism. In libraries, developers design their module trees so that the public API paths are short and intuitive, while internal implementation details remain hidden behind private modules. Understanding paths gives you the power to navigate any Rust codebase—your own or one you have just opened—without guessing where an item lives or why the compiler says it cannot be found.