Error Handling¶
Kiwi provides structured error handling through try, catch, and finally blocks, a throw statement, and the error standard library package for constructing and inspecting typed errors.
Basic Syntax¶
try
# code that might fail
catch e
# e is a hashmap: { error: "...", message: "..." }
finally
# always runs
end
try¶
Wraps code that might throw. If an error is raised, execution jumps to the matching catch block.
catch¶
Handles a thrown error. Parentheses are optional.
| Syntax | What you get |
|---|---|
catch e |
A hashmap { error: "TypeName", message: "..." } |
catch e, m |
e = type string, m = message string |
finally¶
Runs unconditionally after try/catch, whether or not an error was thrown. Useful for cleanup.
catch e — hashmap form¶
The single-parameter form gives you a structured hashmap with error and message keys. Use the error package helpers to inspect it.
try
throw error::ValueError("must be positive")
catch e
println error::type_of(e) # ValueError
println error::message_of(e) # must be positive
end
You can also use e["error"] and e["message"] directly:
try
throw error::NotFoundError("record 42")
catch e
println e["error"] # NotFoundError
println e["message"] # record 42
end
catch e, m — destructured form¶
The two-parameter form destructures the error into a type string and a message string.
try
throw error::TimeoutError("30s exceeded")
catch t, m
println "${t}: ${m}" # TimeoutError: 30s exceeded
end
throw — plain strings¶
You can throw a plain string. In the single-param catch e form it is wrapped in a hashmap with type "KiwiError".
try
throw "something went wrong"
catch e
println e["error"] # KiwiError
println e["message"] # something went wrong
end
In the two-param form:
try
throw "something went wrong"
catch t, m
println t # KiwiError
println m # something went wrong
end
finally block¶
finally always runs — even if an error is thrown and there is no catch block. In that case the error is swallowed and execution continues after the end.
# try/finally without catch — error is swallowed, finally always runs
x = 0
try
x = 1
throw "something failed"
x = 2
finally
x += 100
end
println x # 101
With both catch and finally:
fn read_file(path)
f = null
try
f = fio::open(path, "r")
return fio::read(f)
catch e
eprintln "Read failed: ${error::message_of(e)}"
finally
fio::close(f) when f != null
end
end
Nested try-catch¶
for i in [0, 1] do
try
println "${i / 0}"
catch e
println "loop error: ${error::message_of(e)}"
finally
println "done with iteration ${i}"
end
end
Output:
loop error: Attempted to divide by zero.
done with iteration 0
loop error: Attempted to divide by zero.
done with iteration 1
The error package¶
The error standard library package provides helpers for constructing, throwing, and inspecting structured errors. It is loaded automatically.
Named constructors¶
Each constructor returns a hashmap { error: "TypeName", message: "..." }.
throw error::ValueError("x must be positive")
throw error::NotFoundError("record 42")
throw error::TypeError("expected integer, got string")
Available constructors and their default messages:
| Constructor | Default message |
|---|---|
error::ArgumentError(msg?) |
"Invalid argument." |
error::AssertionError(msg?) |
"Assertion failed." |
error::IndexError(msg?) |
"Index out of bounds." |
error::IOError(msg?) |
"I/O error." |
error::KeyError(msg?) |
"Key not found." |
error::NetworkError(msg?) |
"Network error." |
error::NotFoundError(msg?) |
"Not found." |
error::NotImplementedError(msg?) |
"Not implemented." |
error::OverflowError(msg?) |
"Overflow." |
error::ParseError(msg?) |
"Parse error." |
error::PermissionError(msg?) |
"Permission denied." |
error::TimeoutError(msg?) |
"Operation timed out." |
error::TypeError(msg?) |
"Type error." |
error::ValueError(msg?) |
"Invalid value." |
error::new(type, message)¶
Creates a structured error hashmap with custom type and message strings.
e = error::new("MyError", "something went wrong")
# { error: "MyError", message: "something went wrong" }
Defaults: type = "Error", message = "An error occurred."
Inspection helpers¶
nfe = error::NotFoundError("item 99")
error::type_of(nfe) # "NotFoundError"
error::message_of(nfe) # "item 99"
error::is_type(nfe, "NotFoundError") # true
error::is_type(nfe, "IOError") # false
These helpers also accept plain strings (as from a two-param catch):
try
throw error::KeyError("name")
catch t, m
println error::type_of(t) # KeyError
println error::message_of(m) # name
end
Guard helpers¶
error::assert(condition, msg?) — throws AssertionError if condition is false.
error::expect_type(val, expected_type, name?) — throws TypeError if val.type() does not match expected_type.
fn add(a, b)
error::expect_type(a, "integer", "a")
error::expect_type(b, "integer", "b")
return a + b
end
error::expect_not_null(val, name?) — throws ValueError if val is null.
Matching error types in catch¶
try
throw error::KeyError("user_id")
catch e
if error::is_type(e, "KeyError")
println "missing key: ${error::message_of(e)}"
elsif error::is_type(e, "NotFoundError")
println "not found: ${error::message_of(e)}"
else
throw e # re-throw unhandled errors
end
end
Re-throwing errors¶
Catch an error and re-throw it if you can't handle it at the current level.
try
risky_operation()
catch e
eprintln "Unexpected: ${error::type_of(e)}: ${error::message_of(e)}"
throw e
end
when guard with throw¶
The when guard can be used with throw to raise an error conditionally in a single expression.