Question
Global Variables in Rust: statics, shared state, and better alternatives
Question
I understand that global variables are usually discouraged, but in some practical cases they can seem useful, especially when a value is central to the whole program.
While learning Rust, I am writing a small database test program using SQLite. In this example, I currently pass the database connection between many functions. Since there are around a dozen functions involved, I wondered whether using a global variable would be possible instead.
Is it possible, feasible, or advisable to use global variables in Rust?
Given code like this:
extern crate sqlite;
fn main() {
let db: sqlite::Connection = open_database();
if !insert_data(&db, insert_max) {
return;
}
}
Can I declare and use a global variable for the database connection?
I tried something like this:
extern crate sqlite;
static mut DB: Option<sqlite::Connection> = None;
fn main() {
DB = sqlite::open("test.db").expect("'test.db' should be readable");
println!("Database opened successfully");
create_table();
println!("Completed");
}
fn create_table() {
let sql = "CREATE TABLE IF NOT EXISTS TEMP2 (ikey INTEGER PRIMARY KEY NOT NULL)";
match DB.exec(sql) {
Ok(_) => println!("Table created"),
Err(err) => println!("Execution of SQL failed: {}\nSql={}", err, sql),
}
}
But this produces compile errors such as:
error[E0308]: mismatched types
expected enum `std::option::Option`, found struct `sqlite::Connection`
error: no method named `exec` found for type `std::option::Option<sqlite::Connection>`
How should this be done correctly in Rust, and what is the idiomatic approach?
Short Answer
By the end of this page, you will understand how global variables work in Rust, why static mut is usually a poor choice, and why passing a database connection by reference is often the idiomatic design. You will also learn safer patterns for global shared state, such as static, lazy initialization, and synchronization types like Mutex and OnceLock.
Concept
Rust does support global data, but not in the same casual way as many other languages.
The main reason is safety. A mutable global variable can be read and written from many places, which makes code harder to reason about and can cause data races in concurrent programs. Rust is designed to prevent that.
The key global concepts in Rust
const
- Compile-time constant
- Inlined where used
- Cannot hold runtime-created values like a database connection
static
- A value stored in one fixed memory location for the entire program
- Can be global
- Usually immutable
Example:
static APP_NAME: &str = "My App";
static mut
- A mutable global variable
- Access is
unsafe - Very easy to misuse
- Usually avoided in idiomatic Rust
Example:
static mut COUNTER: i32 = 0;
This exists, but Rust makes it inconvenient on purpose because unrestricted mutable global state is dangerous.
Why your example fails
Your declaration is:
static mut DB: Option<sqlite::Connection> = None;
That means DB is an Option<sqlite::Connection>, not a plain sqlite::Connection.
So this assignment does not match:
DB = sqlite::open("test.db").expect("...");
You are trying to assign a sqlite::Connection into an Option<sqlite::Connection>. It would need to be wrapped in Some(...).
Also, this line fails:
DB.exec(sql)
because DB is an Option, and Option itself does not have an exec method. You would first need to unwrap or match on the Option.
Why a database connection is usually not a good global
A database connection is runtime state. It may:
- fail to open
- need controlled ownership
- require cleanup
- not be thread-safe
- need borrowing rules enforced
Passing &Connection to functions is often the best design because:
- ownership is clear
- dependencies are explicit
- testing is easier
- functions are easier to reuse
That is why Rust code often prefers this style:
fn create_table(db: &sqlite::Connection) {
let sql = "CREATE TABLE IF NOT EXISTS TEMP2 (ikey INTEGER PRIMARY KEY NOT NULL)";
match db.exec(sql) {
Ok(_) => println!("Table created"),
Err(err) => println!("Execution of SQL failed: {}", err),
}
}
Idiomatic rule of thumb
- If you can pass a reference or store state in a struct, do that.
- If you truly need a global value, prefer safe global patterns like
OnceLock,LazyLock, or a synchronized wrapper such asMutex<T>. - Avoid
static mutunless you are working at a very low level and understand the risks fully.
Mental Model
Think of a Rust program like a workshop.
- A normal local variable is a tool on one worker's bench.
- Passing
&dbto a function is like handing the tool to another worker for a moment. - A global mutable variable is like putting one shared tool in the middle of the room and letting anyone grab and modify it at any time.
That shared tool sounds convenient, but it creates problems:
- Who is using it right now?
- Is someone changing it while another part of the program depends on it?
- Has it even been set up yet?
Rust tries to make shared mutable tools harder to use unless you add the right safety controls. That is why globals exist, but safe Rust prefers clearly passing references or using synchronized shared state.
Syntax and Examples
Preferred approach: pass the connection by reference
This is the most idiomatic solution for your example.
extern crate sqlite;
fn main() {
let db = sqlite::open("test.db").expect("'test.db' should be readable");
println!("Database opened successfully");
create_table(&db);
println!("Completed");
}
fn create_table(db: &sqlite::Connection) {
let sql = "CREATE TABLE IF NOT EXISTS TEMP2 (ikey INTEGER PRIMARY KEY NOT NULL)";
match db.exec(sql) {
Ok(_) => println!("Table created"),
Err(err) => println!("Execution of SQL failed: {}\nSql={}", err, sql),
}
}
Why this is good
- No global mutable state
- No
unsafe - Easy to understand
- Easy to test
If you really want a global: safe one-time initialization
In modern Rust, a safer pattern is using OnceLock with synchronization when needed.
use std::sync::{Mutex, OnceLock};
static DB_PATH: OnceLock<String> = OnceLock::new();
fn main() {
DB_PATH.set("test.db".to_string()).expect("DB_PATH already initialized");
println!("Database path set");
create_table();
}
fn create_table() {
let path = DB_PATH.get().expect("DB_PATH not initialized");
println!("Would open database at: {}", path);
}
This example uses a string because not all database connection types are suitable for plain global storage. Whether a connection can be safely stored globally depends on the type's thread-safety and initialization rules.
Why your original Option idea needed different syntax
Your global was declared as an Option:
static mut DB: Option<sqlite::Connection> = None;
So assigning would have to look more like:
DB = Some(sqlite::open("test.db").expect("'test.db' should be readable"));
And using it would require matching or unwrapping:
match DB {
Some(ref db) => {
db.exec("SELECT 1").unwrap();
}
None => {
println!("Database not initialized");
}
}
But because static mut access is unsafe, this is still not the recommended design.
Step by Step Execution
Consider this version:
extern crate sqlite;
fn main() {
let db = sqlite::open("test.db").expect("database should open");
create_table(&db);
}
fn create_table(db: &sqlite::Connection) {
let sql = "CREATE TABLE IF NOT EXISTS TEMP2 (ikey INTEGER PRIMARY KEY NOT NULL)";
db.exec(sql).expect("table creation should succeed");
}
Step by step
1. main starts
let db = sqlite::open("test.db").expect("database should open");
sqlite::open("test.db")tries to create a connection.- It returns a result-like value.
.expect(...)either extracts the connection or stops the program with an error message.dbnow owns the database connection.
2. Pass a reference to the function
create_table(&db);
&dbmeans "borrow the connection".- Ownership stays in
main. create_tablecan use the connection without taking it away.
3. Build the SQL string
let sql = "CREATE TABLE IF NOT EXISTS TEMP2 (ikey INTEGER PRIMARY KEY NOT NULL)";
- This is a string slice stored in the program.
- It describes the SQL command to run.
4. Execute the SQL
db.exec(sql).expect("table creation should succeed");
- The connection runs the SQL statement.
- If execution fails,
expectstops the program with the message.
5. Function ends
- The borrow of
dbends whencreate_tablefinishes. mainstill owns the connection and could pass it to other functions.
This is the important idea: borrowing lets many functions use the same value without turning it into a global variable.
Real World Use Cases
Where global state appears in real Rust programs
Global state is sometimes useful for data that is truly application-wide, such as:
- configuration loaded once at startup
- logging setup
- metrics counters
- feature flags
- caches shared across the program
- singleton-like registries
Where passing references is usually better
For runtime resources like database connections, developers usually prefer:
- passing
&Connectionto helper functions - storing the connection inside an
AppStatestruct - passing that state through application layers
Example:
struct AppState {
db: sqlite::Connection,
}
fn create_table(state: &AppState) {
let sql = "CREATE TABLE IF NOT EXISTS TEMP2 (ikey INTEGER PRIMARY KEY NOT NULL)";
state.db.exec(sql).unwrap();
}
This is common in command-line tools, servers, and web applications because it keeps dependencies explicit.
In web apps and services
A real application might use:
- a connection pool shared through app state
- immutable global configuration
- a global logger
But a single mutable raw connection as a global is usually avoided unless carefully synchronized.
Real Codebase Usage
In real Rust codebases, developers usually avoid naked globals and use structured state patterns.
Common patterns
1. Dependency passing
Pass required values into functions.
fn insert_user(db: &sqlite::Connection, name: &str) {
let sql = format!("INSERT INTO users (name) VALUES ('{}')", name);
db.exec(&sql).unwrap();
}
This keeps dependencies obvious.
2. App state struct
Group shared resources together.
struct AppState {
db: sqlite::Connection,
app_name: String,
}
This scales better than adding more globals.
3. Guard clauses and initialization checks
If global state is used, code often checks initialization early.
use std::sync::OnceLock;
static CONFIG: OnceLock<String> = OnceLock::new();
fn get_config() -> &'static str {
CONFIG.get().map(|s| s.as_str()).expect("CONFIG not initialized")
}
4. Error handling instead of assuming globals exist
Instead of assuming a global is ready, code often returns Result.
fn maybe_get_setting() -> Result<&'static str, &'static str> {
CONFIG.get().map(|s| s.as_str()).ok_or("CONFIG not initialized")
}
5. Synchronization for mutable shared state
If mutation is necessary, real code often wraps the value in Mutex<T> or RwLock<T>.
This is much safer than static mut because access is controlled.
Common Mistakes
1. Assigning a plain value into an Option<T>
Broken code:
static mut DB: Option<sqlite::Connection> = None;
fn main() {
DB = sqlite::open("test.db").expect("open failed");
}
Problem:
DBisOption<sqlite::Connection>- the right side is
sqlite::Connection
Fix:
DB = Some(sqlite::open("test.db").expect("open failed"));
2. Calling methods on Option<T> instead of on T
Broken code:
DB.exec(sql)
Problem:
execbelongs tosqlite::ConnectionDBis anOption<sqlite::Connection>
Fix:
match DB {
Some(ref db) => db.exec(sql).unwrap(),
None => panic!("DB not initialized"),
}
3. Using static mut as if it were normal mutable state
Broken idea:
- reading and writing
static mutanywhere - assuming it is safe because the program looks simple
Problem:
static mutis unsafe because Rust cannot guarantee exclusive access- this can become undefined behavior in concurrent or aliased access situations
4. Making everything global instead of designing state
A common beginner shortcut is turning shared data into globals because passing parameters feels repetitive.
Better options:
- pass references
- create an
AppStatestruct - group related functions into methods
5. Ignoring ownership and borrowing benefits
Passing &db may seem inconvenient at first, but it is exactly how Rust keeps code safe and predictable.
6. Using a single global connection where a pool or scoped resource is better
In real applications, a single shared connection can become a bottleneck or a design problem. Even if a global is technically possible, it may not be the right architecture.
Comparisons
| Approach | Can hold runtime value? | Mutable? | Safe by default? | Good for DB connection? | Notes |
|---|---|---|---|---|---|
const | No | No | Yes | No | Compile-time only |
static | Yes | No | Yes | Sometimes | Best for immutable global data |
static mut | Yes | Yes | No | Usually no | Requires unsafe |
OnceLock<T> | Yes | Usually set once | Yes | Sometimes | Good for one-time initialization |
Mutex<T> inside static | Yes | Yes | Yes | Maybe | Safer shared mutable access |
Pass &T to functions | Yes | Through borrowing rules | Yes | Yes | Most idiomatic in many cases |
Store in AppState struct | Yes | Yes, if needed | Yes | Yes | Scales well in real projects |
static mut vs passing references
-
static mut- hidden dependency
- unsafe access
- harder to test
- harder to reason about
-
passing references
- explicit dependency
- safe borrowing
- easier to test
- clearer function design
Best practical choice for your example
For a SQLite connection used by many functions, the usual choice is:
- pass
&sqlite::Connection, or - store it in an application state struct and pass
&AppState.
That is generally better than a global mutable variable.
Cheat Sheet
Quick reference
Immutable global
static NAME: &str = "app";
Mutable global with static mut
static mut VALUE: i32 = 0;
- Requires
unsafeto read or write - Usually avoid in application code
One-time global initialization
use std::sync::OnceLock;
static CONFIG: OnceLock<String> = OnceLock::new();
Safer shared mutable global
use std::sync::{Mutex, OnceLock};
static DATA: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
Important rules
constis for compile-time constantsstaticis for program-lifetime storagestatic mutis unsafeOption<T>means the value may or may not exist- To store a value in
Option<T>, wrap it inSome(...) - To use the inner value, match on it or unwrap it
For your exact error
Wrong:
DB = sqlite::open("test.db").expect("open failed");
Right shape:
DB = Some(sqlite::open("test.db").expect("open failed"));
Idiomatic recommendation
Prefer:
fn create_table(db: &sqlite::Connection) { /* ... */ }
instead of a global mutable database connection.
FAQ
Can Rust have global variables?
Yes. Rust supports global data through const, static, and static mut. However, mutable globals are intentionally restricted because they can be unsafe.
Why is static mut discouraged in Rust?
Because it allows shared mutable state, which can lead to data races, aliasing problems, and undefined behavior. Rust prefers safer patterns like borrowing, Mutex, and OnceLock.
Why did my Option<Connection> code fail?
Because you tried to assign a Connection directly into an Option<Connection>, and then called a Connection method on the Option itself. You need Some(connection) and then must unwrap or match the Option.
Should I use a global database connection in Rust?
Usually no. It is more idiomatic to pass &Connection to functions or store the connection inside an app state struct.
What is the idiomatic alternative to global variables in Rust?
Pass values by reference, or group shared resources into a struct such as AppState and pass that struct around.
When is a global value acceptable in Rust?
Globals are reasonable for immutable configuration, logging setup, metrics, or other application-wide values that are initialized once and accessed safely.
Can I safely initialize a global value at runtime?
Yes, with tools like OnceLock or LazyLock. These are safer than static mut for one-time initialization.
Is passing &db to many functions normal in Rust?
Yes. It is a very common and idiomatic pattern. It makes dependencies explicit and keeps ownership clear.
Mini Project
Description
Build a small Rust program that opens a SQLite database once and passes the connection to helper functions instead of using a global mutable variable. This demonstrates the idiomatic Rust approach to sharing important runtime state across multiple functions.
Goal
Create a SQLite-backed program that initializes a table and inserts data by passing the database connection as a reference.
Requirements
- Open a SQLite database in
main - Pass the connection by reference to at least two helper functions
- Create a table if it does not already exist
- Insert at least one row into the table
- Print a success message for each operation
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.