The ProblemComing from languages like Python or JavaScript, you might expect to initialize a global configuration map by simply calling a constructor. Rust, however, is much more protective of the compilation process. It distinguishes strictly between what happens when you build your code and what happens when it runs.
Here is the snippet that usually triggers this headache:
use std::collections::HashMap;
// This will trigger E0015
const MY_CONFIG: HashMap = HashMap::new();
fn main() {
println!("{:?}", MY_CONFIG);
}
Run cargo build, and the compiler will stop you with this message:
error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
--> src/main.rs:4:39
|
4 | const MY_CONFIG: HashMap = HashMap::new();
| ^^^^^^^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants
Why This HappensThe const keyword tells Rust to calculate a value once at compile time and inline it everywhere it is used. To do this safely, the compiler requires that any function called within a const block is marked as a const fn. These are deterministic functions that the compiler can execute without running your full program.
You might wonder why HashMap::new() isn't a const fn. The reason is that a HashMap uses a random seed for its hashing algorithm to prevent DoS attacks. Generating that randomness requires interacting with the operating system, which the compiler cannot do while it's still building your binary.
Solution 1: Use LazyLock (The Modern Standard)If you are using Rust 1.80 or newer, std::sync::LazyLock is your best friend. It allows you to define a global variable that waits to initialize until the exact moment you first access it. This bypasses compile-time restrictions while remaining thread-safe.
use std::collections::HashMap;
use std::sync::LazyLock;
// LazyLock initializes only when MY_CONFIG is first used
static MY_CONFIG: LazyLock> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert("api_version", 2);
m.insert("timeout_ms", 5000);
m
});
fn main() {
// The HashMap is actually created here
println!("Timeout: {:?}", MY_CONFIG.get("timeout_ms"));
}
Note that we switched from const to static. This ensures the data lives in a single, fixed memory location rather than being copied everywhere.
Solution 2: Use OnceLock for Dynamic DataSometimes you don't know the values of your global variable until the program is already running—perhaps you're reading a DATABASE_URL from an environment variable. In these cases, std::sync::OnceLock is the better choice.
use std::collections::HashMap;
use std::sync::OnceLock;
static APP_CACHE: OnceLock> = OnceLock::new();
fn main() {
// Initialize the cache once at runtime
let cache = APP_CACHE.get_or_init(|| {
let mut m = HashMap::new();
m.insert("session_id".to_string(), "abc-123".to_string());
m
});
println!("Current session: {}", cache["session_id"]);
}
Solution 3: Converting Custom Functions to 'const'If you hit E0015 while calling your own function, you can often fix it by adding the const modifier. This works as long as your function doesn't perform heap allocation or complex I/O.
// Adding 'const' allows this to run during compilation
const fn get_max_threads(cores: u32) -> u32 {
cores * 2
}
const WORKER_LIMIT: u32 = get_max_threads(8);
fn main() {
println!("Max workers: {}", WORKER_LIMIT);
}
Solution 4: Use Static Arrays for Zero OverheadFor small, fixed lookups, a HashMap might be overkill. A static array of tuples is often faster and much easier for the compiler to handle. This approach uses zero runtime initialization time.
// A simple array of pairs is often enough for constants
const SERVER_SETTINGS: [(&str, u16); 3] = [
("web", 8080),
("api", 9000),
("metrics", 9100),
];
fn main() {
for (service, port) in SERVER_SETTINGS {
println!("Service {} is on port {}", service, port);
}
}

