Error Handling
Things go wrong in scripts - users provide invalid input, files don't exist, network requests fail. Generally, for smaller CLI scripts, it's okay if we just exit on the spot, and that's also Rad's default behavior. However, if you wish to more gracefully handle these errors or attempt recovery, Rad gives you the tools to do so.
In Rad, errors are values. Functions that might fail return either a result or an error, and you can decide how to handle them. This makes error handling explicit, predictable, and easy to reason about.
In this section, we'll explore:
- Error propagation - how errors bubble up by default (and why scripts exit)
- The
catch:block - handle errors with full control (logging, reassignment, exit) - The
??operator - shorthand for simple fallback values - Creating errors - using
error()in your own functions - Error type unions - making fallible operations explicit with
T|errortype annotations
Error Propagation¶
Let's start with a simple script that takes a user's age as input:
| File: age | |
|---|---|
1 2 3 4 5 | |
In reality, you'd instead declare the arg as an int and let Rad handle input validation,
but for the purposes of this guide, we write it as a str.
If we run this with a valid number, everything works:
rad age 25
You are 25 years old
But what happens with invalid input?
rad age "not-a-number"
Error at L4:7
age = parse_int(age_str)
^^^^^^^^^^^^^^^^^^ parse_int() failed to parse "not-a-number" (RAD20001)
The script exits immediately with an error code of 1 when parse_int encounters invalid input.
What's happening is that parse_int returned an error value, and since we're not handling it, it immediately gets propagated up.
Since we're at the root of the script and not nested within any other function call, this triggers a script exit on the spot.
Nested Calls¶
Errors don't just propagate from built-in functions - they bubble up through your own function calls too. Here's an example:
1 2 3 4 5 6 7 8 9 10 | |
Error at L2:13
price = parse_float(price_str) // Error starts here...
^^^^^^^^^^^^^^^^^^^^^^
parse_float() failed to parse "invalid" (RAD20002)
The error originates in parse_float, propagates through calculate_discount, then through process_order, and finally exits at the top level. At any point in this chain, we could choose to handle the error instead of letting it propagate.
This sets up the question: how do we handle errors gracefully instead of crashing?
Catch Blocks¶
The catch: block gives you full control over error handling. Attach it as a suffix to any expression that might error, and you can inspect the error, log it, provide a fallback value, or decide whether to exit.
Basic Error Handling¶
Here's how to handle our age parsing example gracefully:
| File: age | |
|---|---|
1 2 3 4 5 6 7 8 | |
Now when we run it with invalid input:
rad age "not-a-number"
Invalid age, falling back to 0: parse_int() failed to parse "not-a-number"
Age: 0
The script continues running with our fallback value. Inside the catch: block, the age variable contains the
error string, as returned by parse_int, which we can log or inspect. We then reassign age to a sensible default value of 0.
To summarize:
- Suffix form: write
... catch:directly after the error-able expression. - Binding: the target variable is first bound to the error value; inside the block, interpolating it (e.g.
{age}) prints the error’s message. - Control: you can log, reassign a fallback, or exit(code).
- Flow: execution continues after the block unless you exit.
Exiting on Errors¶
Sometimes you want to fail fast - handle the error just enough to log a helpful message, then exit:
| File: readconfig | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
Running this with a non-existent file:
rad readconfig "missing.txt"
Failed to read config: open missing.txt: no such file or directory
This example is not much better than the default error propagation and exit, but you can imagine providing more useful guidance to users in a more detailed error message.
Ignoring Errors with pass¶
Sometimes you want to ignore errors entirely - the operation might fail, but that's perfectly fine and requires no action:
1 2 3 4 5 | |
Here, pass does nothing - it's a way to explicitly say "I know this might error, but I don't care." This is useful for cleanup operations where the failure itself is harmless.
The ?? Operator¶
For simple cases where you just want a default value without any logging or conditional logic, the ?? operator provides a concise shorthand:
age = parse_int(age_str) ?? 0
timeout = parse_int(get_env("TIMEOUT")) ?? 30
max_retries = parse_int(config["retries"]) ?? 5
The ?? operator uses lazy evaluation - the right side is only evaluated if the left side returns an error. This means you can even call functions on the right:
config = read_file(config_path) ?? get_default_config()
Comparing ?? and catch¶
These two are roughly equivalent:
// Using ??
age = parse_int(age_str) ?? 0
// Using catch:
age = parse_int(age_str) catch:
age = 0
But catch: lets you log errors or do conditional handling:
age = parse_int(age_str) catch:
print_err("Invalid age '{age_str}': {age}")
age = 0
When to use which
Use ?? when you just need a default value and don't care about logging or inspecting the error.
Use catch: when you need to log the error, perform conditional logic, including whether to exit.
Creating Your Own Errors¶
When writing your own functions, you can return errors using the error(str) function.
If you're using type annotations, then functions that may return errors should reflect that in its return type: T|error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Invalid port: Port must be between 1-65535, got 99999
Our custom error message provides clear feedback about what went wrong.
By returning int|error, the type signature tells you three things:
- This function normally returns an
int - It might return an
errorinstead - Callers should consider handling the error case (otherwise let it propagate)
This pattern is used throughout Rad's built-in functions:
parse_int(str) -> int|errorparse_float(str) -> float|errorread_file(path) -> error|{ "size_bytes": int, "content": str }round(num, decimals) -> error|int|float
The error union makes your code self-documenting - anyone reading your function signature knows immediately that it can fail.
More on Union Types
We covered union types in detail in an earlier section: Type Annotations. Error unions are just one application of Rad's union type system.
Summary¶
Rad's error handling model gives you the tools to write robust scripts that handle failures gracefully:
- Errors are values that propagate by default, unless handled
- Scripts exit if errors propagate up to the root of the script
catch:blocks provide full error handling control:- Variable contains the error string inside the block
- You can log errors, provide fallbacks, or call
exit() - Execution continues unless you explicitly exit
??operator provides concise fallbacks with lazy evaluation- Use for simple cases without logging
- Right side only evaluated if left side errors
- Create errors with
error("message")in your own functions - Type unions (
T|error) make fallible operations explicit in function signatures
Next¶
CLI scripts and the shell go hand in hand, and Rad offers first-class support for invoking shell commands and handling its output. We explore this in the next section: Shell Commands.