Skip to main content

Null Safety

Oxide provides comprehensive null safety through optional types and safe-handling operators. Types are non-nullable by default.

Nullable Types

Use T? to create a nullable (optional) type:

let name: String? = "Alice"   // Has a value
let empty: String? = null // No value

Transpiles to:

let name: Option<String> = Some("Alice".to_string());
let empty: Option<String> = None;

The null Keyword

Use null instead of None:

var value: Int? = 42
value = null

fn findUser(id: Int): User? {
if id == 0 {
return null
}
return database.getUser(id)
}

Transpiles to:

let mut value: Option<isize> = Some(42);
value = None;

fn find_user(id: isize) -> Option<User> {
if id == 0 {
return None;
}
return database.get_user(id);
}

Implicit Wrapping

Non-null values are automatically wrapped when needed:

let x: Int? = 42              // Automatically Some(42)
fn getValue(): Int? = 100 // Automatically Some(100)

Transpiles to:

let x: Option<isize> = Some(42);
fn get_value() -> Option<isize> { Some(100) }

Safe-Handling Operators

Not-Null Assertion (!!)

Unwraps the value, panicking if null:

let name: String? = "Bob"
let unwrapped: String = name!! // "Bob"

let empty: String? = null
// empty!! // Panics!

Transpiles to:

let name: Option<String> = Some("Bob".to_string());
let unwrapped: String = name.unwrap();
warning

Only use !! when you're certain the value is not null. Prefer safer alternatives.

Null Coalescing (??)

Provides a default value when null:

let name: String? = null
let result = name ?? "Default" // "Default"

let value: Int? = 42
let result = value ?? 0 // 42

// Chain with complex defaults
let config = loadConfig() ?? Config.default()

Transpiles to:

let name: Option<String> = None;
let result = name.unwrap_or_else(|| "Default".to_string());

let value: Option<isize> = Some(42);
let result = value.unwrap_or(0);

Safe Call (?.)

Calls methods only if non-null, propagating null otherwise:

let name: String? = "hello"
let len: Int? = name?.len() // Some(5)

let empty: String? = null
let len: Int? = empty?.len() // null

// Chain safe calls
let first: Char? = name?.chars()?.next()

// Access fields safely
let city: String? = user?.address?.city

Transpiles to:

let name: Option<String> = Some("hello".to_string());
let len: Option<usize> = name.as_ref().map(|v| v.len());

let empty: Option<String> = None;
let len: Option<usize> = empty.as_ref().map(|v| v.len());

Smart Casting

After a null check, the type is automatically narrowed:

let value: Int? = getValue()

if value != null {
// value is now Int (not Int?)
let doubled: Int = value * 2
println!("Doubled: $doubled")
}

// Also works with pattern matching
if value is Some(v) {
println!("Got: $v")
}

Transpiles to:

let value: Option<isize> = get_value();

if let Some(value) = value {
let doubled: isize = value * 2;
println!("Doubled: {}", doubled);
}

Optional Binding

If-Let

if let name = optionalName {
println!("Hello, $name")
} else {
println!("No name provided")
}

Guard-Let

For early returns:

fn process(value: Int?) {
guard let v = value else {
println!("No value")
return
}
// v is non-optional here
println!("Processing $v")
}

Pattern Matching with Null

Use null directly in match patterns:

fn describe(opt: Int?): String {
match opt {
null -> "No value"
Some(0) -> "Zero"
Some(n) if n > 0 -> "Positive: $n"
Some(n) -> "Negative: $n"
}
}

Transpiles to:

fn describe(opt: Option<isize>) -> String {
match opt {
None => "No value".to_string(),
Some(0) => "Zero".to_string(),
Some(n) if n > 0 => format!("Positive: {}", n),
Some(n) => format!("Negative: {}", n),
}
}

Option Extension Methods

The standard library provides idiomatic methods:

let opt: Int? = 42

// Null checks (idiomatic)
opt != null // true
opt == null // false

// Transformations
opt.unwrapOr(0) // 42
opt.mapped { it * 2 } // Some(84)
opt.flatMapped { getNext(it) }
opt.filtered { it > 10 } // Some(42)

// Unwrap with closure default
opt.unwrapOrElse { computeDefault() }

See Standard Library - Option for all methods.

Combining Operators

// Safe call + null coalesce
let length = name?.len() ?? 0

// Safe call chain + null coalesce
let city = user?.address?.city ?? "Unknown"

// Safe call + method chain
let upper = name?.uppercased()?.trimmed()

// Conditional with safe call
if user?.isAdmin() ?? false {
showAdminPanel()
}

Rust Interoperability

T? and Option<T> are fully compatible:

// Call Rust function returning Option
let result: Int? = rustFunction()

// Pass Oxide nullable to Rust
fn rustExpectsOption(opt: Option<Int>) { ... }
let value: Int? = 42
rustExpectsOption(value) // Works seamlessly

Best Practices

  1. Prefer ?? over !! - Provide meaningful defaults
  2. Use ?. for chains - Avoid nested if-lets
  3. Use guard-let for early returns - Cleaner than nested ifs
  4. Use smart casting - Let the compiler narrow types
// Good: Safe with default
let name = user?.name ?? "Guest"

// Good: Early return pattern
fn processUser(user: User?) {
guard let u = user else { return }
// Work with non-optional u
}

// Avoid: Unnecessary force unwrap
// let name = user!!.name // Might panic

See Also