Skip to content
📖 Auto vs RustBook contents

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:

  • ?T represents a value that might be absent — the equivalent of Rust's Option<T>. It has two variants: Some(T) and None.
  • !T represents an operation that might fail — the equivalent of Rust's Result<T, E> (where E is always String). It has two variants: Ok(T) and Err(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 produced value
  • None — 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:

A function that returns `?int` and the `??` null coalescing operator

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:

Pattern matching on `?int` with `is`

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.

?T vs Option<T> Quick Reference ​

AutoRustMeaning
?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 succeeded
  • Err(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:

Extracting a value from `!T` with `.collect()`

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:

Error propagation with `fn main() !` and `.collect()`

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.

!T vs Result<T, E> Quick Reference ​

AutoRustMeaning
!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:

Using `None` and `Some` for optional return values

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():

Using `.expect()` for unrecoverable errors

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:

ConceptAutoRust
Nullable value?TOption<T>
Error result!TResult<T, E>
Unwrap or defaultx ?? defaultx.unwrap_or(default)
Propagate errorx.!x?
Panic with message.expect("msg").expect("msg")
Error-returning mainfn main() !fn main() -> Result<(), String>
Pattern matchx is { ... }match x { ... }

The key improvements Auto makes over Rust:

  1. Simpler type syntax: ?int instead of Option<i32>, !int instead of Result<i32, String>.
  2. Null coalescing: x ?? 0 replaces the verbose x.unwrap_or(0).
  3. Consistent error type: !T always uses String for errors, eliminating the complexity of generic error types.
  4. Pattern matching: is replaces match with 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.

Released under the MIT License.