Skip to content
Kavachकवच

Tasks: When a task fails

Also known as: unhandled promise rejection unobserved task exception fire and forget swallowed error lost error background task failed

● Advanced4 min

A task's error comes out of .wait(), like the error of an ordinary call, and an error nobody reads is printed, not lost.

fn parse_age(text: String) -> Int ! ParseError {
    try Int(text)
}

fn main() {
    const good = spawn parse_age("31")
    const bad = spawn parse_age("thirty")
    print(good.wait() catch _ => -1)
    print(bad.wait() catch e => {
        print("the task failed: {e}")
        -1
    })
}

The program ran and printed:

kavach run wait-error.kav

31the task failed: `thirty` is not a valid Int-1
wait-error.kav: Kavach code, then what running it prints

.wait() gives the task's result or its error; try passes it on, catch handles it. Calling .wait() again gives the same.

An error nobody reads

A task that failed and is let go of before any .wait() read its error doesn't lose it: the error goes to standard error, with where the task was spawned. Then the program goes on, and its exit code doesn't change:

use std.time

fn save(report: String) -> Int ! Error {
    fail Error("disk full")
}

fn main() uses Console, Clock {
    // `_`: nobody waits for this task, on purpose
    const _saving = spawn save("weekly report")
    time.sleep(10.ms)       // the task runs and fails meanwhile
    print("main goes on")
}
src/main.kav: Kavach code
terminal
kavach run src/main.kav
main goes on
error in a task nobody waited for: disk full (spawned at src/main.kav:9)
  • A task is let go of when the function that spawned it returns. A Task handed out to other code is let go of when its last holder lets it go; in native code, at the latest when the program ends.
  • An error that a .wait() read, as in the first example, is not printed again.
  • An agent's event that runs again after a restart says so instead, with the event's number: error in an event run again after a restart: …. See the audit log.

A spawn on a line of its own starts a task nobody waits for, as go f() does in Go. spawn save(r) is the same as const _t = spawn save(r):

fn save(report: String) -> Int ! Error {
    fail Error("disk full")
}

fn greet(name: String) uses Console {
    print("hello, {name}")
}

fn main() uses Console {
    spawn greet("ann")              // nobody waits for it
    spawn save("weekly report")     // nor for this one, and its error is printed
    print("main goes on")
}
src/main.kav: Kavach code
terminal
kavach run src/main.kav
main goes on
hello, ann
error in a task nobody waited for: disk full (spawned at src/main.kav:11)
  • The tasks are still cancelled when main returns. A task cancelled that way stops at its next wait; these two have none, so they finish first.
  • An error in such a task is still printed, with where it was spawned.

A Task kept in a name that is never used gets a warning. A name starting with _, as in _saving above, says nobody waits for it on purpose and silences the warning.

fn save(report: String) -> Int ! Error {
    fail Error("disk full")
}

fn main() {
    const saving = spawn save("weekly report")
}

The compiler warned, then the program ran:

kavach run task-unused.kav

warning: `saving` is never used: remove it, or start its name with `_` if that is on purpose
task-unused.kav: Kavach code, then what running it prints

A panic is not an error, but a panic in a task stops only that task: it is printed when it happens, and .wait() fails with panicked: <message> at <file>:<line>. See Panics.