Error Returns
Errors happen—files don't exist, networks fail, parsing goes wrong. Coco's error handling is designed to be explicit, safe, and not annoying.
Error Unions
Functions that can fail return an error union type, written as !T:
fn read_file($path string): !string {
if !file_exists($path) {
return error("File not found: {$path}");
}
return fs::read($path);
}
The !string means "returns a string or an error." You can't ignore it.
Creating Errors
Use the error() function to create errors:
// Simple message
return error("Something went wrong");
// Formatted message
return error("Invalid value: {$value}");
// Wrap an existing error with context
return error.wrap($err, "Failed to process file");
Handling Errors
You have several options for handling error returns.
Auto-Propagation
If you don't handle an error, it propagates automatically:
fn process_user($id i32): !User {
$config = read_config("app.conf"); // If this fails...
$user = fetch_user($id); // ...error propagates up
return $user;
}
No try, no ?, no boilerplate. If read_config returns an error, process_user returns that error immediately.
The catch Keyword
Handle errors inline with catch:
$content = read_file($path) catch $err {
echo "Error: {$err}";
return "";
};
The catch block receives the error and must either:
- Return a value of the expected type
- Return an error
- Use
continue,break, orreturn
Match on Result
For more control, match on the result:
match read_file($path) {
Ok($content) => process($content),
Err($e) => {
log_error($e);
use_default()
}
}
Provide a Default
Use or for simple defaults:
$content = read_file($path) or "";
$config = load_config() or Config::default();
Chaining Errors
Add context as errors bubble up:
fn process_order($id i32): !Order {
$data = fetch_order_data($id) catch $err {
return error.wrap($err, "Failed to fetch order {$id}");
};
$parsed = parse_order($data) catch $err {
return error.wrap($err, "Failed to parse order {$id}");
};
return $parsed;
}
When this fails, you get a chain: "Failed to process order 42: Failed to parse order 42: Invalid JSON at line 15"
Multiple Return Values (Go-style)
For compatibility or when you prefer explicit handling:
fn read_file_go($path string): (string, ?Error) {
if !file_exists($path) {
return ("", Error::new("File not found"));
}
return (fs::read($path), null);
}
// Caller
$content, $err = read_file_go($path);
if $err != null {
echo "Error: {$err}";
return;
}
process($content);
This style is more verbose but some teams prefer the explicit error variable.
Retry Patterns
Combine error handling with loops:
fn fetch_with_retry($url string): !Response {
for $attempt in 0..3 {
$response = http_get($url) catch $err {
if $attempt == 2 {
return error.wrap($err, "Failed after 3 attempts");
}
sleep(Duration::secs(1));
continue;
};
return Ok($response);
}
return error("Unreachable");
}
Error Types
You can define custom error types:
enum DatabaseError {
ConnectionFailed { host: string },
QueryFailed { query: string, reason: string },
NotFound { table: string, id: i32 }
}
fn find_user($id i32): Result<User, DatabaseError> {
// ...
}
Custom error types enable exhaustive matching:
match find_user($id) {
Ok($user) => show($user),
Err(ConnectionFailed { host }) => retry_connection(host),
Err(QueryFailed { query, reason }) => log_query_error(query, reason),
Err(NotFound { .. }) => create_new_user($id)
}
The error Trait
Any type can be an error if it implements the Error trait:
type MyError {
message string
code i32
}
impl Error for MyError {
fn message(&$this): string {
return $this.message;
}
}
Assertions and Panics
For programmer errors (bugs), use assertions:
fn get_index($arr &[i32], $i i32): i32 {
assert($i >= 0 && $i < $arr.len(), "Index out of bounds");
return $arr[$i];
}
Use panic() for unrecoverable situations:
fn must_have_config(): Config {
$config = load_config() or {
panic("Configuration required");
};
return $config;
}
Panics are for bugs, not for expected errors. Expected errors should use !T.
Best Practices
Use error unions for expected failures. Network errors, file not found, invalid input—these should use !T.
Use panics for bugs. Invariant violations, "impossible" states—these are programmer errors.
Add context when wrapping. error.wrap($err, "while processing user") is more helpful than just propagating.
Keep error messages actionable. "Failed to connect to database at localhost:5432" is better than "Database error."
Don't over-catch. Let errors propagate when you can't do anything useful with them.
// Bad - catching just to re-throw
$data = fetch() catch $err {
return error($err);
};
// Good - just let it propagate
$data = fetch();
Match exhaustively on custom error types. The compiler will tell you if you miss a case.