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.
| Kind | When | Does the program run? | For example |
|---|---|---|---|
| Compile error | before the program runs | No: fix it first | a misspelt name |
| Warning | before the program runs | Yes | a variable nobody uses |
| Error (a failure value) | while it runs, and expected | Yes, the code handles it | text that isn't a number |
| Panic | while it runs: a bug | No: it stops at once | dividing 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`?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
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
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
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
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
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
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
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)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)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)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
127A 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)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)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)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
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:
ParseError | Reading text as a number, Bool or Char: Int(text) | spec |
FsError | Files: NotFound(path), Denied(path), Failed(path, reason) | spec |
NetError | Connections: Refused, TimedOut, Failed | spec |
HttpError | HTTP: the network, a bad URL, a bad message, an error status, or JSON that doesn't fit | spec |
JsonError | JSON: bad syntax (with line and column), too deep, too large, a repeated key, or every place the text doesn't fit the type | spec |
GrpcError | gRPC calls | spec |
RegexError | A pattern that isn't valid | spec |
CliError | Command-line arguments that don't fit | spec |
AiError | Asking an AI model: no key set, the provider refused, an answer of the wrong shape, or a limit said no | spec |
Error | Any error at all: the catch-all for scripts and main | spec |
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! 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`To find every function that can fail with an error type, directly or passed on through ! Error, ask the compiler:
src/main.kav:8 fn read_settings(path: String) -> String ! FsError src/main.kav:16 fn load(path: String) -> Settings ! Error
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
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`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 typeIn a library, still prefer a specific error type such as ! FsError: the compiler then checks that every case is handled, with no _ needed.