Error Handling
Flint treats errors as values. There are no exceptions, no stack unwinding, and no throw. Functions that can fail say so in their return type, and you handle the failure at the call site.
Result<T, E> and Option<T>
These two types cover the error model:
// A function that might fail
fn read_config(path: string) -> Result<Config, Error> { ... }
// A function that might not find a value
fn lookup(key: string) -> Option<string> { ... }
Constructors:
return Ok(value) // success
return Ok() // success with unit value
return Err("message") // failure with Error
return Some(value) // present
return None // absent
The ? Operator
? is shorthand for: “if this is Err or None, return it immediately from the enclosing function.”
With Result
fn process(path: string) -> Result<(), Error> {
let text = io.read_text(path)? // propagates Err automatically
let config = Config.parse(text)? // propagates Err automatically
apply(config)
return Ok()
}
Without ? it would be:
fn process(path: string) -> Result<(), Error> {
let text = match io.read_text(path) {
Ok(t) => t
Err(e) => return Err(e)
}
// ...
}
With Option
? also works with Option<T>. In that case, None returns early from the enclosing function:
fn read_port() -> Option<u16> {
let raw = env.lookup("PORT")?
let parsed = i32.parse(raw).ok()?
return u16.try_from(parsed).ok()
}
This example returns:
NoneifPORTis missingNoneif parsing failsNoneif the parsed value does not fit inu16Some(port)on success
The enclosing function must return a compatible Result or Option.
The Error Type
Error is Flint’s standard general-purpose error type. It is always in scope with no import needed. It is equivalent to an immutable string message:
fn connect(addr: string) -> Result<Connection, Error> {
if addr.is_empty() {
return Err("address cannot be empty")
}
// ...
}
You can use domain-specific error enums for richer modeling:
enum ParseError {
UnexpectedEof
UnexpectedToken { found: string, expected: string }
InvalidNumber { text: string }
}
fn parse(src: string) -> Result<Ast, ParseError> { ... }
Result and Option Methods
You do not always need a full match. Option<T> and Result<T, E> have a small set of built-in helpers for the most common cases.
Check the State
Use these when you only need to ask whether a value is present or successful:
let found = some_option.is_some()
let missing = some_option.is_none()
let ok = some_result.is_ok()
let failed = some_result.is_err()
Provide a Default
Use unwrap_or when you want a fallback value instead of branching:
let port = maybe_port.unwrap_or(8080)
let retries = parsed_retries.unwrap_or(3)
Transform the Success Value
Use map when you want to keep the container shape and transform only the success or present value:
let upper = username.map(|name| {
return name.upper()
}) // Option<string>
let doubled = reading.map(|value| {
return value * 2
}) // Result<u16, Error>
Chain Another Fallible Step
Use and_then when the closure itself returns another Option or Result:
let home_dir = env.lookup("HOME").and_then(|path| {
return normalize_path(path)
}) // Option<string>
let config = io.read_text("flint.toml").and_then(|text| {
return Config.parse(text)
}) // Result<Config, Error>
Convert Option Into Result
Use ok_or when absence should become an error:
let port = env.lookup("PORT").ok_or("PORT is required")
Transform the Error Value
Use map_err when the success case is fine but you want a different error type:
let reading = read_sensor().map_err(|err| -> SensorError {
return SensorError.ReadFailed { message: err }
})
Recover From an Error
Use or_else when you want to replace one failure with another attempt:
let config = load_primary_config().or_else(|_| {
return load_backup_config()
})
These methods cover most day-to-day cases:
- inspect:
is_some,is_none,is_ok,is_err - default:
unwrap_or - transform success:
map - chain another step:
and_then - convert absence into failure:
ok_or - transform failure:
map_err - recover from failure:
or_else
If the logic starts branching in several directions, switch back to match. The methods are for simple pipelines, not for hiding complex control flow.
Fatal Errors
For bugs and unrecoverable states, not for normal error handling:
// Crash with a message (never returns)
fatal("invariant violated: queue empty")
// Crash with an Error value
fatal_error(err)
// Assert a condition (crashes if false)
assert(buffer.len() <= MAX_SIZE)
// Mark unreachable code paths
match direction {
Direction.Left => turn_left()
Direction.Right => turn_right()
_ => unreachable() // tells compiler this cannot happen
}
Fatal errors do not unwind. On MCU targets they emit a compact diagnostic record and then trap. They are a last-resort debugging path, not a substitute for Result.
Conventions
- Functions that can fail at runtime return
Result<T, Error>or a domain-specificResult<T, MyError>. - Functions that return optional data return
Option<T>. - Do not return
Resultfor operations that cannot fail. Unnecessary noise makes code harder to read. - Use
?liberally to propagate errors up the call stack without boilerplate. - Use
fatalandassertfor bug detection, not for expected runtime conditions.
// Good
fn parse_port(s: string) -> Result<u16, Error> {
let n = i32.parse(s)?
return u16.try_from(n).ok_or("port out of range")
}
// Avoid: defensive Result for operations that cannot fail
fn add(a: i32, b: i32) -> Result<i32, Error> { // unnecessary
return Ok(a + b)
}