Error Handling in Lateralus
Lateralus uses Result[T, E] for recoverable errors and panics for unrecoverable ones. The ? operator propagates errors through pipelines naturally. This post covers the error handling philosophy and practical patterns.
◉ The Problem with Exceptions
Exception-based languages (Java, Python, C++) have hidden control flow:
// Java: any of these might throw, but nothing tells you
String content = readFile(path);
Data data = parse(content);
Result result = process(data);
You can't tell from the code what errors are possible. The compiler doesn't help. Errors are invisible until runtime.
◉ Result Type
Lateralus makes errors explicit with Result[T, E]:
enum Result[T, E] {
Ok(T),
Err(E),
}
fn read_file(path: str) -> Result[str, IOError] {
// Returns Ok(content) or Err(IOError)
}
fn parse(content: str) -> Result[Data, ParseError] {
// Returns Ok(data) or Err(ParseError)
}
The return type tells you exactly what can go wrong. The compiler forces you to handle it.
◉ The ? Operator
Handling errors explicitly can be verbose:
fn process_file(path: str) -> Result[Data, Error] {
let content = match read_file(path) {
Ok(c) => c,
Err(e) => return Err(e.into()),
}
let data = match parse(content) {
Ok(d) => d,
Err(e) => return Err(e.into()),
}
Ok(transform(data))
}
The ? operator eliminates boilerplate:
fn process_file(path: str) -> Result[Data, Error] {
let content = read_file(path)?
let data = parse(content)?
Ok(transform(data))
}
// Or with pipelines:
fn process_file(path: str) -> Result[Data, Error] {
read_file(path)?
|> parse()?
|> transform()
|> Ok
}
The ? operator:
- If
Ok(v), unwraps tov - If
Err(e), returnsErr(e.into())from the function
◉ Error Context
When errors bubble up, context is lost. Where did it fail? The .context() method adds information:
fn load_config(path: str) -> Result[Config, Error] {
let content = read_file(path)
.context("reading config file")?
let parsed = parse_toml(content)
.context("parsing config TOML")?
let config = validate_config(parsed)
.context("validating config")?
Ok(config)
}
// Error message:
// Error: validating config
// Caused by: missing required field 'database.url'
// While: parsing config TOML
// While: reading config file "/etc/app/config.toml"
◉ Error Types
Define domain-specific error types:
enum NetworkError {
Timeout { host: str, after_ms: int },
ConnectionRefused { host: str, port: int },
DNSError { name: str },
TLSError { reason: str },
}
impl NetworkError {
fn message(self) -> str {
match self {
Timeout { host, after_ms } =>
"Connection to " + host + " timed out after " + str(after_ms) + "ms",
ConnectionRefused { host, port } =>
"Connection refused to " + host + ":" + str(port),
// ...
}
}
}
◉ Error Conversion
Convert between error types with From trait:
impl From[IOError] for AppError {
fn from(e: IOError) -> AppError {
AppError::IO(e)
}
}
impl From[ParseError] for AppError {
fn from(e: ParseError) -> AppError {
AppError::Parse(e)
}
}
// Now ? automatically converts:
fn load_data(path: str) -> Result[Data, AppError] {
let content = read_file(path)? // IOError -> AppError
let data = parse(content)? // ParseError -> AppError
Ok(data)
}
◉ Option Type
For "might not exist" without errors:
enum Option[T] {
Some(T),
None,
}
fn find_user(id: int) -> Option[User] {
// Returns Some(user) or None
}
// Use with match
match find_user(123) {
Some(user) => println("Found: " + user.name),
None => println("User not found"),
}
// Or with ? in Option-returning functions
fn get_user_email(id: int) -> Option[str] {
let user = find_user(id)?
user.email // Might also be Option
}
◉ Combining Results and Options
// Convert Option to Result
let user = find_user(123)
.ok_or(Error::UserNotFound(123))?
// Convert Result to Option (discarding error)
let user = load_user(123).ok()
◉ Panics
For unrecoverable errors:
fn divide(a: int, b: int) -> int {
if b == 0 {
panic("division by zero")
}
a / b
}
// Or with assertions
fn process(data: [int]) {
assert(!data.is_empty(), "data cannot be empty")
// ...
}
Panics are for bugs, not expected conditions. They crash the program (or thread in async context).
◉ Pipeline Error Handling
Multiple patterns for pipelines:
// Early return on first error
let result = items
|> map(fn(i) { process(i)? })
|> collect()?
// Collect all errors
let (successes, failures) = items
|> map(process)
|> partition_results()
// Filter out errors, keep successes
let results = items
|> map(process)
|> filter_ok()
|> collect()
◉ Best Practices
- Use Result for recoverable errors — file not found, network timeout, invalid input
- Use Option for "might not exist" — map lookup, search results
- Use panic for bugs — violated invariants, impossible states
- Add context at boundaries — where errors cross module lines
- Define domain error types — don't use String for everything
Full error handling documentation in the language guide.