Docs/Language basics

Null values

Represent a missing value and check for it safely.

On this page

A ? before a type, as in ?String or ?User, allows the value null. The compiler makes you check such a value before you use it.

Check for null#

Valk
fn greet(name: ?String) String {
    if isset(name) {
        return "Hello, " + name   // name is a String here
    }
    return "Hello, guest"
}

fn main() {
    println(greet("Ada"))  // Hello, Ada
    println(greet(null))   // Hello, guest
}

isset(x) is true when x is not null, and inside the branch x has its non-null type. !isset(x) checks the opposite.

Return early#

After a check that leaves the function, the rest of it sees the value as non-null:

Valk
fn initial(name: ?String) String {
    if !isset(name) : return "?"
    return name.range(0, 1)
}

fn main() {
    println(initial("Zed")) // Z
    println(initial(null))  // ?
}

The same works with break, continue, throw and panic.

Fall back to a default#

Valk
fn main() {
    let nickname: ?String = null
    println(nickname ?? "Anonymous")   // Anonymous
}

a ?? b gives a when it is set and b otherwise. It binds tighter than comparisons: count ?? 0 == 3 means (count ?? 0) == 3.

Leave when null#

?! unwraps a value or runs what follows it, which must leave the current flow:

Valk
fn first_word(text: ?String) String {
    let value = text ?! return ""
    return value.split(" ").get(0) !? ""
}

fn main() {
    println(first_word("hello world")) // hello
    let required: ?String = "ok"
    let checked = required ?! panic("missing value")
    println(checked)                   // ok
}

Read through a chain#

?. reads a member of a value that may be null. If any step is null, the whole chain is null:

Valk
class Address { city: String }
class User { address: ?Address (null) }

fn find_user(id: int) ?User {
    if id == 1 : return User { address: Address { city: "London" } }
    return null
}

fn main() {
    println(find_user(1)?.address?.city ?? "unknown") // London
    println(find_user(2)?.address?.city ?? "unknown") // unknown
}

The result is nullable, so it usually ends in ?? or ?!. A method call without a result, such as user?.save(), is skipped when the value is null. You cannot assign through ?..

Null versus errors#

  • ?? and ?! handle a missing value.
  • !? and ! handle an error from a call.

A function that cannot find something can return ?T when absence is normal, or throw an error when the caller should handle it explicitly. Map lookups show both: map["key"] gives null (or zero) for a missing key, while map.get("key") throws. See Maps.