Skip to content
Kavachकवच

Kinds of Errors

Also known as: error types kinds of errors compile error warning runtime error exception panic crash bug parse error file not found network error

● Intermediate6 minBefore this: Reading ErrorsSpec 5

Kavach reports problems in four ways: compile errors and warnings before the program runs, errors and panics while it runs.

KindWhenDoes the program run?For example
Compile errorbefore the program runsNo: fix it firsta misspelt name
Warningbefore the program runsYesa variable nobody uses
Error (a failure value)while it runs, and expectedYes, the code handles ittext that isn't a number
Panicwhile it runs: a bugNo: it stops at oncedividing by zero

1. Compile errors

The compiler refuses the program, says what is wrong, and usually what to write instead. Nothing runs.

fn main() {
    const total = 40
    print(totl + 2)
}

The compiler refused the program:

kavach run typo.kav

error: `totl` is not defined; did you mean `total`?
typo.kav: Kavach code, then what running it prints

2. Warnings

The program runs, but the compiler points at something that is probably a mistake, with the fix:

fn main() {
    let draft = "not used yet"
    print("still runs")
}

The compiler warned, then the program ran:

kavach run unused.kav

warning: `draft` is never used: remove it, or start its name with `_` if that is on purposestill runs
unused.kav: Kavach code, then what running it prints
fn main() {
    let a = 0.1
    print(a + 0.2 == 0.3)
}

The compiler warned, then the program ran:

kavach run float-eq.kav

warning: `==` on floats compares every bit, and rounding makes most computed values differ (`0.1 + 0.2` worked out while running isn't `0.3`): use `a.near(b)`, or `Decimal` for exact amountsfalse
float-eq.kav: Kavach code, then what running it prints
fn main() {
    const n = -3
    print(n % 2 == 1)
}

The compiler warned, then the program ran:

kavach run odd.kav

warning: `x % 2 == 1` is wrong for a negative odd `x`, since `-3 % 2` is `-1`: write `x % 2 != 0`false
odd.kav: Kavach code, then what running it prints
fn main() {
    const two = Int("2") catch 0
    print(two)
}

The compiler warned, then the program ran:

kavach run catch-nothing.kav

warning: `catch` does nothing here: this can't fail2
catch-nothing.kav: Kavach code, then what running it prints

3. Errors: failures you expect

Bad input, a missing file, a network that is down: these are normal, so they are values the program handles. A function that can fail says so with !, and the caller must deal with it (the Errors page shows how):

fn main() {
    for text in ["42", "4 2", "abc"] {
        const n = Int(text) catch e => {
            print("{text}: {e}")
            continue
        }
        print(n)
    }
}

The program ran and printed:

kavach run parse.kav

424 2: `4 2` is not a valid Intabc: `abc` is not a valid Int
parse.kav: Kavach code, then what running it prints
use std.fs

fn main() {
    const text = fs.read_text("no-such-file.txt") catch e => {
        print("could not read: {e}")
        ""
    }
    print(text.len())
}

The program ran and printed:

kavach run file.kav

could not read: NotFound(path: "no-such-file.txt")0
file.kav: Kavach code, then what running it prints

4. Panics: bugs

A panic means the program itself is wrong: an index past the end, a division by zero, a broken contract. It stops at once instead of carrying on with a wrong value:

fn share(total: Int, people: Int) -> Int {
    total / people
}

fn main() {
    print(share(100, 4))
    print(share(100, 0))
}

The program ran, then stopped with a panic:

kavach run share.kav

25panic: division by zero
share.kav: Kavach code, then what running it prints

When the compiler already knows the values, it doesn't wait for the program to run: arithmetic that would always panic is a compile error.

fn main() {
    const total = 100
    const people = 0
    print(total / people)
}

The compiler refused the program:

kavach run divzero.kav

error: this always panics: division by zero. Divide by something that can't be 0 (spec 1.8)
divzero.kav: Kavach code, then what running it prints

The compiler follows a let too, while nothing in between may change it:

fn main() {
    let count: Int8 = 120
    count += 7
    count += 1
    print(count)
}

The compiler refused the program:

kavach run let-known.kav

error: this always panics: integer overflow: 127 + 1 doesn't fit in Int8 (spec 1.8)
let-known.kav: Kavach code, then what running it prints

Not only arithmetic: .unwrap() on an optional the compiler knows is still none would always panic, so it is caught the same way:

fn main() {
    let found: Int? = none
    print(found.unwrap())
}

The compiler refused the program:

kavach run unwrap-known.kav

error: this always panics: `.unwrap()` on `none`. Handle `none` with `if let`, `match` or `?? default` (spec 1.8)
unwrap-known.kav: Kavach code, then what running it prints

A let is known no more once a branch, a loop or a lambda may set it, once an if, while or match tests it, or once it is passed to an inout parameter. The test may be what keeps the arithmetic safe, so this one compiles and runs:

fn main() {
    let count: Int8 = 127
    if count < 127 {
        count += 1
    }
    print(count)
}

The program ran and printed:

kavach run let-tested.kav

127
let-tested.kav: Kavach code, then what running it prints

A for over a range of known numbers counts too, when nothing in its body can leave early. Each statement that always runs is checked for the loop's first and last values, and the message is that of the first round that panics:

fn main() {
    const sizes = [10, 20, 30]
    for i in 0..4 {
        print(sizes[i])
    }
}

The compiler refused the program:

kavach run loop-known.kav

error: this always panics: index 3 is out of range: the list has 3 items (spec 1.8)
loop-known.kav: Kavach code, then what running it prints

A += or -= of a known number is checked for all its rounds:

fn main() {
    let hits: Int8 = 0
    for _ in 0..200 {
        hits += 1
    }
    print(hits)
}

The compiler refused the program:

kavach run loop-adds.kav

error: this always panics: integer overflow: 127 + 1 doesn't fit in Int8 (spec 1.8)
loop-adds.kav: Kavach code, then what running it prints

A known whole number converted to a whole-number type it fits stays known, so this is caught too:

fn main() {
    for i in 0..100 {
        print(Int8(i) * 2)
    }
}

The compiler refused the program:

kavach run loop-convert.kav

error: this always panics: integer overflow: 64 * 2 doesn't fit in Int8 (spec 1.8)
loop-convert.kav: Kavach code, then what running it prints

A break, continue, return, fail or try in the body may be what keeps that round from running, so the loop is not checked. Nor is a statement inside an if, a match or an inner loop that may not run:

fn main() {
    const sizes = [10, 20, 30]
    for i in 0..4 {
        if i == sizes.len() {
            break
        }
        print(sizes[i])
    }
}

The program ran and printed:

kavach run loop-break.kav

102030
loop-break.kav: Kavach code, then what running it prints

The rule of thumb: if it can happen in a correct program (the user typed rubbish), it is an error. If it can only happen in a wrong program, it is a panic.

Error types from the standard library

Each module that can fail has its own error type, a union you can match on:

ParseErrorReading text as a number, Bool or Char: Int(text)spec
FsErrorFiles: NotFound(path), Denied(path), Failed(path, reason)spec
NetErrorConnections: Refused, TimedOut, Failedspec
HttpErrorHTTP: the network, a bad URL, a bad message, an error status, or JSON that doesn't fitspec
JsonErrorJSON: bad syntax (with line and column), too deep, too large, a repeated key, or every place the text doesn't fit the typespec
GrpcErrorgRPC callsspec
RegexErrorA pattern that isn't validspec
CliErrorCommand-line arguments that don't fitspec
AiErrorAsking an AI model: no key set, the provider refused, an answer of the wrong shape, or a limit said nospec
ErrorAny error at all: the catch-all for scripts and mainspec

Calls to a capability, such as Payments.refund, have one more case. Any of them can be refused, by a limit, by a person who says no, or because nothing implements the capability, so every capability's error type has the case Denied(reason: String), and the compiler insists on it. See When a call is refused.

Which error can it be?

Other languages make you guess which exceptions a call can throw, or test the type of the error at run time. In Kavach the first line of a function names its error type, so you know before you call it:

use std.fs

fn load(path: String) -> String ! FsError {
    try fs.read_text(path)
}

fn main() {
    const text = load("settings.toml") catch e => match e {
        FsError.NotFound(path) => "no {path} yet: using the defaults",
        FsError.Denied(path) => "not allowed to read {path}",
        FsError.Failed(path, reason) => "{path} is broken: {reason}",
    }
    print(text)
}

The program ran and printed:

kavach run which.kav

no settings.toml yet: using the defaults
which.kav: Kavach code, then what running it prints

! FsError says load can only fail with a file error, and match handles each case. Leave one out, and the compiler names it:

use std.fs

fn load(path: String) -> String ! FsError {
    try fs.read_text(path)
}

fn main() {
    const text = load("settings.toml") catch e => match e {
        FsError.NotFound(path) => "no {path} yet: using the defaults",
        FsError.Denied(path) => "not allowed to read {path}",
    }
    print(text)
}

The compiler refused the program:

kavach run which-missed.kav

error: this `match` does not cover `FsError.Failed`
which-missed.kav: Kavach code, then what running it prints

To find every function that can fail with an error type, directly or passed on through ! Error, ask the compiler:

terminal
kavach query fails FsError
src/main.kav:8  fn read_settings(path: String) -> String ! FsError
src/main.kav:16  fn load(path: String) -> Settings ! Error
kavach query fails JsonError
src/main.kav:12  fn parse_settings(text: String) -> Settings ! JsonError
src/main.kav:16  fn load(path: String) -> Settings ! Error

Which error is inside an Error?

! Error accepts any error. A caller can still ask which one it holds, as Python's except FileNotFoundError or Go's errors.As do: name the type with each case.

use std.fs
use std.json

type Settings {
    port: Int
}

fn load(path: String) -> Settings ! Error {
    const text = try fs.read_text(path)
    try json.decode<Settings>(text)
}

fn main() ! Error {
    try fs.write_text("broken.json", "\{\"port\": 80")
    for path in ["missing.json", "broken.json"] {
        const settings = load(path) catch e => match e {
            FsError.NotFound(p) => {
                print("{p} isn't there: using port 8080")
                Settings(port: 8080)
            }
            JsonError.Syntax(line, column, _) => {
                print("{path} is broken at line {line}, column {column}: using port 8080")
                Settings(port: 8080)
            }
            _ => fail e,
        }
        print(settings.port)
    }
}

The program ran and printed:

kavach run match-error.kav

missing.json isn't there: using port 80808080broken.json is broken at line 1, column 12: using port 80808080
match-error.kav: Kavach code, then what running it prints

An Error can hold any error type, so each case names its type (FsError.NotFound), and a _ arm handles the rest; here it passes the error on with fail e. Both are checked:

use std.fs

fn load(path: String) -> String ! Error {
    try fs.read_text(path)
}

fn main() {
    const text = load("app.toml") catch e => match e {
        NotFound(p) => "no {p}",
        _ => "something else",
    }
    print(text)
}

The compiler refused the program:

kavach run match-error-bare.kav

error: an `Error` can hold any error type: write `FsError.NotFound`
match-error-bare.kav: Kavach code, then what running it prints
use std.fs

fn load(path: String) -> String ! Error {
    try fs.read_text(path)
}

fn main() {
    const text = load("app.toml") catch e => match e {
        FsError.NotFound(p) => "no {p}",
    }
    print(text)
}

The compiler refused the program:

kavach run match-error-no-default.kav

error: a `match` on an `Error` needs a `_ => …` arm: an `Error` can hold any error type
match-error-no-default.kav: Kavach code, then what running it prints

In a library, still prefer a specific error type such as ! FsError: the compiler then checks that every case is handled, with no _ needed.

Check yourself

QUIZ 1 / 1pick one
A user types "abc" where your program asked for a number. What should that be?