Question

Global Variables in Rust: statics, shared state, and better alternatives

rustglobal-variables

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 as Mutex<T>.
  • Avoid static mut unless 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 &db to 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.

Take Quiz

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.
  • db now owns the database connection.

2. Pass a reference to the function

create_table(&db);
  • &db means "borrow the connection".
  • Ownership stays in main.
  • create_table can 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, expect stops the program with the message.

5. Function ends

  • The borrow of db ends when create_table finishes.
  • main still 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 &Connection to helper functions
  • storing the connection inside an AppState struct
  • 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.

Take Quiz

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:

  • DB is Option<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:

  • exec belongs to sqlite::Connection
  • DB is an Option<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 mut anywhere
  • assuming it is safe because the program looks simple

Problem:

  • static mut is 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 AppState struct
  • 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

ApproachCan hold runtime value?Mutable?Safe by default?Good for DB connection?Notes
constNoNoYesNoCompile-time only
staticYesNoYesSometimesBest for immutable global data
static mutYesYesNoUsually noRequires unsafe
OnceLock<T>YesUsually set onceYesSometimesGood for one-time initialization
Mutex<T> inside staticYesYesYesMaybeSafer shared mutable access
Pass &T to functionsYesThrough borrowing rulesYesYesMost idiomatic in many cases
Store in AppState structYesYes, if neededYesYesScales 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.

Take Quiz

Cheat Sheet

Quick reference

Immutable global

static NAME: &str = "app";

Mutable global with static mut

static mut VALUE: i32 = 0;
  • Requires unsafe to 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

  • const is for compile-time constants
  • static is for program-lifetime storage
  • static mut is unsafe
  • Option<T> means the value may or may not exist
  • To store a value in Option<T>, wrap it in Some(...)
  • 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.

Take Quiz

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
Take Quiz

Keep learning

Related questions

View more