Error Handling in Atlas 77
Atlas 77 provides explicit error handling through optional<T> and expected<T, E> types. There is no implicit error propagation; errors must be handled explicitly to avoid “hidden magic.”
Philosophy
Error handling in Atlas 77 is explicit and intentional:
- No automatic exception throwing or catching
- No implicit
try/?operator for error propagation - Errors must be checked and handled at the point they occur
- This gives you maximum control over error recovery
optional Type
The optional<T> type represents a value that may or may not exist.
Creating optional Values
Use optional<T> for fallible operations that don’t need error details:
import "std/optional";
struct User {
public:
id: int64;
name: string;
User(id: int64, name: string) {
this.id = id;
this.name = name;
}
}
fun find_user(id: int64) -> optional<User> {
if id > 0 {
// Found user
let user = new User(id, "Alice");
return optional<User>::of(user);
} else {
// Not found
return optional<User>::empty();
}
}
Checking optional Values
let user_opt = find_user(42);
// Check if value exists
if user_opt.has_value() {
let user = user_opt.value(); // Consumes the optional
println(user.name);
} else {
println("User not found");
}
Using value_or for Defaults
import "std/optional";
fun get_config_value(key: string) -> optional<string> {
// ... lookup logic ...
return optional<string>::empty();
}
fun main() {
let timeout = get_config_value("timeout").value_or("30");
println(timeout); // Prints: 30
}
expected Type
The expected<T, E> type represents either a success with a value, or a failure with an error.
Creating expected Values
Use expected<T, E> when operations can fail and you need to communicate why:
import "std/expected";
fun parse_int(s: string) -> expected<int64, string> {
// Try to parse string to integer
if is_valid_number(s) {
let number: int64 = convert_to_int(s);
return expected<int64, string>::expect(number);
} else {
return expected<int64, string>::unexpected("Invalid number format");
}
}
Checking expected Values
let result = parse_int("42");
// Explicit check with is_expected
if result.is_expected() {
let number = result.expected_value(); // Consumes the expected
println(number);
} else {
let error = result.unexpected_value(); // Consumes the expected
println(error);
}
Using expected_value_or for Defaults
import "std/expected";
fun divide(a: int64, b: int64) -> expected<int64, string> {
if b == 0 {
return expected<int64, string>::unexpected("Division by zero");
}
return expected<int64, string>::expect(a / b);
}
fun main() {
let result = divide(10, 0).expected_value_or(0);
println(result); // Prints: 0
}
Pattern: Checking Both Success and Error
let result = parse_int("invalid");
if result.is_expected() {
let value = result.expected_value();
println(value);
} else if result.is_unexpected() {
let error = result.unexpected_value();
println(error); // Prints: Invalid number format
}
Panic
For unrecoverable errors, use panic() to abort the program:
import "std/io";
fun critical_operation() {
if invalid_state {
panic("Critical error: invalid state detected!");
}
}
panic() will:
- Print the error message
- Terminate the program immediately
- Bypass normal cleanup (use sparingly)
Pattern Examples
optional Handling
import "std/optional";
struct User {
public:
id: int64;
name: string;
User(id: int64, name: string) {
this.id = id;
this.name = name;
}
}
fun find_user(id: int64) -> optional<User> {
if id > 0 {
let user = new User(id, "Alice");
return optional<User>::of(user);
}
return optional<User>::empty();
}
fun get_user_name(id: int64) -> optional<string> {
let user_opt = find_user(id);
if user_opt.has_value() {
let user = user_opt.value();
return optional<string>::of(user.name);
} else {
return optional<string>::empty();
}
}
// Usage
fun main() {
let name_opt = get_user_name(1);
if name_opt.has_value() {
println(name_opt.value());
} else {
println("User not found");
}
}
expected Chaining
When multiple fallible operations depend on each other:
import "std/expected";
import "std/fs";
fun read_and_parse() -> expected<int64, string> {
// First operation
let file = new File("numbers.txt");
if !file.exists() {
return expected<int64, string>::unexpected("File not found");
}
let content = file.read();
// Second operation (depends on first)
let number = parse_int(content);
if number.is_unexpected() {
return expected<int64, string>::unexpected(number.unexpected_value());
}
// Success
return expected<int64, string>::expect(number.expected_value());
}
Nested expected with Meaningful Errors
import "std/expected";
struct ParseError {
public:
line: int64;
column: int64;
message: string;
ParseError(line: int64, column: int64, message: string) {
this.line = line;
this.column = column;
this.message = message;
}
}
fun parse_config(path: string) -> expected<Config, ParseError> {
// Try to read file
let file = new File(path);
if !file.exists() {
let error = new ParseError(0, 0, "File not found");
return expected<Config, ParseError>::unexpected(error);
}
let content = file.read();
// Try to parse content
let config = parse_config_text(content);
return config;
}
Best Practices
- Check explicitly – Always use
has_value()/is_expected()/is_unexpected()before consuming values - Provide context – Include helpful error information in
expected<T, E> - Fail fast – Return errors early rather than propagating invalid states
- Use optional for simple cases – When you only need to know if something exists
- Use expected for complex cases – When you need to communicate failure reasons
- Avoid panics in libraries – Let caller decide how to handle errors
- Document error cases – Explain what errors an operation can produce
- Remember consuming methods –
value(),expected_value(), andunexpected_value()consume the container
API Reference
optional Methods
optional<T>::of(data: T) -> optional<T>– Create an optional containing a valueoptional<T>::empty() -> optional<T>– Create an empty optionalhas_value(&this) -> bool– Check if a value is presentvalue(this) -> T– Consume and return the value (panics if empty)value_or(this, default: T) -> T– Consume and return the value or default
expected<T, E> Methods
expected<T, E>::expect(data: T) -> expected<T, E>– Create a successful expectedexpected<T, E>::unexpected(error: E) -> expected<T, E>– Create a failed expectedis_expected(&this) -> bool– Check if this holds a valueis_unexpected(&this) -> bool– Check if this holds an errorexpected_value(this) -> T– Consume and return the value (panics if error)unexpected_value(this) -> E– Consume and return the error (panics if value)expected_value_or(this, default: T) -> T– Consume and return the value or defaultunexpected_value_or(this, default: E) -> E– Consume and return the error or default
Future Improvements
In the future, Atlas 77 may introduce:
- Discriminated unions for more flexible error types
- Error traits for better composability
- More ergonomic error propagation operators
However, the core design will remain explicit and visible, avoiding hidden control flow.
See also:
- Language Reference – Error handling in context
- Memory Model – Resource safety and ownership
- Standard Library – Available error types and utilities