Error Handling
Errors are a fact of life in software. Auto provides two type-level mechanisms for handling situations in which something goes wrong, inspired by Rust's approach but with a more streamlined syntax:
?Trepresents a value that might be absent — the equivalent of Rust'sOption<T>. It has two variants:Some(T)andNone.!Trepresents an operation that might fail — the equivalent of Rust'sResult<T, E>(whereEis alwaysString). It has two variants:Ok(T)andErr(String).
Like Rust, Auto requires you to acknowledge the possibility of an error and take some action before your code will compile. This requirement makes your program more robust by ensuring that you'll discover errors and handle them appropriately.
We'll cover recoverable errors with ?T first, then discuss error propagation with !T, and finally explore when to panic versus when to recover.
Recoverable Errors with ?T (Option)
Most errors aren't serious enough to require the program to stop entirely. Sometimes a function might not have a value to return — for example, looking up an item in a list that doesn't exist, or finding the index of a character that isn't present. In these cases, ?T is the right tool.
The ?T type has two variants:
Some(value)— the operation succeeded and producedvalueNone— the operation failed or produced no value
Returning ?T from Functions
When a function might not have a value to return, declare its return type as ?T:
The ?? operator (null coalescing) is Auto's ergonomic way to unwrap an optional value with a fallback. It replaces Rust's verbose .unwrap_or(default) pattern. If the value is Some(v), it returns v; if it's None, it returns the default.
Pattern Matching on ?T with is
When you need different behavior for Some and None, use the is expression:
The is expression is Auto's pattern matching syntax (replacing Rust's match). It destructures the ?T value: None matches when there's no value, and Some(v) binds the inner value to v.
?TvsOption<T>Quick Reference
Auto Rust Meaning ?intOption<i32>An integer that might be absent Some(42)Some(42)A value is present NoneNoneNo value x ?? 0x.unwrap_or(0)Unwrap with default x is { None => ... }match x { None => ... }Pattern match v[i] ?? defaultv.get(i).copied().unwrap_or(default)Safe list access
Unrecoverable Errors with !T (Result) and Error Propagation
For operations that can fail with an error message (not just "no value"), Auto uses !T. This is equivalent to Rust's Result<T, E> where E is String.
The !T type has two variants:
Ok(value)— the operation succeededErr(message)— the operation failed with a string error message
Extracting Values with .collect()
To get the value out of a !T when you know it should succeed, use .collect() (which transpiles to unwrap() in Rust). If the value is actually an Err, this will panic:
Here, text.to_int() returns !int (Result). The .collect() operator extracts the inner value. If parsing fails, the program panics with the error message.
Error Propagation with .collect() and fn main() !
When you want errors to propagate upward rather than being handled inline, declare your function with ! return type and use .collect() to propagate errors:
The fn main() ! syntax tells Auto that main can return an error. This is equivalent to Rust's fn main() -> Result<(), String>. When the function returns Ok, the program exits with code 0. When it returns Err, the error message is printed and the program exits with a nonzero code.
Key difference from Rust: In Rust, you'd use the ? operator for error propagation. In Auto, .collect() (written as .!) serves this purpose when the error type matches.
!TvsResult<T, E>Quick Reference
Auto Rust Meaning !intResult<i32, String>An int or error message Ok(42)Ok(42)Success value Err("msg")Err("msg".to_string())Error x.!x?Propagate error / unwrap fn foo() !intfn foo() -> Result<i32, String>Error-returning function fn main() !fn main() -> Result<(), String>Error-returning main
Using None and Some with Functions
Functions that return ?T use None for "no value" and Some(value) for a successful result. This is the same as Rust's Option:
This function returns None when dividing by zero (an impossible operation), and Some(result) when the division succeeds. The caller must handle both cases.
Using .expect() for Unrecoverable Errors
When you're certain an operation should succeed and want to panic with a descriptive message if it doesn't, use .expect():
The .expect("message") method works just like in Rust: it returns the inner value if Ok/Some, or panics with the provided message if Err/None.
Use .expect() over .collect() in production code — the message makes debugging much easier when the unexpected happens.
To Panic or Not to Panic
Auto follows the same philosophy as Rust for error handling decisions:
Use ?T (Option) when:
- A value might naturally be absent (e.g., searching a list, looking up a key)
- The caller should decide what to do when there's no value
- Missing values are expected, not exceptional
Use !T (Result) when:
- An operation can fail with a meaningful error message (e.g., parsing, I/O)
- The caller needs to know why something failed
- The error might be recoverable at a higher level
Use .expect() or .collect() when:
- Failure indicates a bug in your code
- You have more information than the compiler (e.g., a hardcoded valid value)
- In tests and prototypes where error handling would obscure the logic
Use fn main() ! when:
- Your top-level code performs operations that can fail
- You want errors to propagate to the program's exit rather than being silently ignored
- You prefer clean error messages over crash diagnostics
Guidelines for Error Handling
It's advisable to have your code panic (via .expect() or .collect() on a failing value) when it's possible that your code could end up in a bad state:
- The bad state is unexpected, as opposed to something that will likely happen occasionally (like a user entering invalid input).
- Your code after this point needs to rely on not being in this bad state.
- There's no good way to encode this information in the types you use.
When failure is expected, it's more appropriate to return ?T or !T than to panic. Examples include a parser being given malformed data or a user input validation check failing.
In production-quality code, most Auto programmers choose .expect("reason") rather than bare .collect() to give more context about why the operation is expected to always succeed. That way, if your assumptions are ever proven wrong, you have more information for debugging.
Summary
Auto's error handling is built on two type-level constructs that map directly to Rust's error types:
| Concept | Auto | Rust |
|---|---|---|
| Nullable value | ?T | Option<T> |
| Error result | !T | Result<T, E> |
| Unwrap or default | x ?? default | x.unwrap_or(default) |
| Propagate error | x.! | x? |
| Panic with message | .expect("msg") | .expect("msg") |
| Error-returning main | fn main() ! | fn main() -> Result<(), String> |
| Pattern match | x is { ... } | match x { ... } |
The key improvements Auto makes over Rust:
- Simpler type syntax:
?intinstead ofOption<i32>,!intinstead ofResult<i32, String>. - Null coalescing:
x ?? 0replaces the verbosex.unwrap_or(0). - Consistent error type:
!Talways usesStringfor errors, eliminating the complexity of generic error types. - Pattern matching:
isreplacesmatchwith a cleaner, more intuitive syntax.
In the next chapter, we'll discuss generic types and how to define your own types with ?T or !T parameters.