Panicking
Panic in Helix
Panic in HelixIn Helix, panic
is a special statement used to signal an error by returning it as the function’s state. It works differently from exceptions in other languages, behaving closer to the return
statement that carries an error instead of a value, while still propagating errors through the type system.
panic
requires the function’s return type to be:
- A questionable type (
?
) - Or a standard error type (
std::Error::<T, E...>
), where:T
is the result type.E
is one or more possible error types. (can be any type or a derivative ofstd::BaseError
)
How Panic Works
How Panic WorksWith Questionable Types (?
)
With Questionable Types (?)When a function’s return type is a questionable type, panic
sets the return value to an error. This value can then be validated using ...?
or error in ...
syntax.
Refer to the Questionable Types Guide for more details on questionable types.
import std::errors;
fn divide(a: int, b: int) -> int? { if b == 0: panic errors::ParseError("Cannot divide by zero");
return a / b;}
fn main() { let result: int? = divide(10, 0);
if errors::ParseError in result { print("Error: Division by zero."); } else if result? { print(f"Result: {result}"); }}
With std::Error::<T, E...>
With std::Error::<T, E...>When using std::Error
, panic
sets the error state, and valid results must be explicitly accessed with the *
operator.
import std::errors;
fn divide(a: int, b: int) -> std::Error::<int, errors::ParseError> { if b == 0: panic errors::ParseError("Cannot divide by zero");
return a / b;}
fn main() -> int { let result: std::Error::<int, errors::ParseError> = divide(10, 0);
if result.has_value() { let value = *result; print(f"Result: {value}"); } else { print("Error: Division by zero."); }
return 0;}
Example of Invalid Panic Usage
Example of Invalid Panic Usageimport std::errors;
fn divide(a: int, b: int) -> int? { if b == 0: panic errors::ParseError("Division by zero");
return a / b;}
fn main() { // we are trying to downgrade the int? to an int this will // crash the program IF the int? does not have a value. // (in this case it does not)
let result: int = divide(10, 0); // program will crash here, implicitly. print(f"Result: {result}");}
What’s Wrong:
- The function
divide
returns anint?
but is assigned to anint
. - This is valid since
int?
can be implicitly converted toint
. However thats only the case if theint?
has a value. - If the
int?
does not have a value, the program will crash. if you try to use it as anint
.
import std::errors;
fn oo_we() -> int { panic errors::ParseError("Look at me, I'm a pickle!");}
fn main() { let result = oo_we(); // would fail compilation. print(f"Result: {result}");}
What’s Wrong:
- The function
oo_we
returns anint
but has apanic
statement. This will fail compilation. - The return type of a function must have either a questionable type or a
std::Error
type if the function can panic.
Examples of Panic Usage
Examples of Panic Usageimport std::errors;
fn parse_age(input: string) -> int? { let age: int? = input as int?; if !(age?): panic errors::ParseError("Invalid age provided");
return age;}
fn main() { let age = parse_age("not-a-number");
if errors::ParseError in age: print("Failed to parse age."); else if age?: print(f"Valid age: {age}");}
What It Does:
- Validates user input and panics with an error if parsing fails.
- Demonstrates error handling with questionable types.
fn divide(a: int, b: int) -> std::Error::<int, errors::DivideByZero> { if b == 0 { panic errors::DivideByZero("Division by zero"); }
return a / b;}
fn calculate(a: int, b: int) -> std::Error::<int, errors::DivideByZero> { let result = divide(a, b);
if result.has_error() { return result.error(); }
return *result * 2; // requires explicit value extraction with `*`. (will crash if the result is an error)}
fn main() { let final_result = calculate(10, 0);
if final_result.has_value() { print(f"Final result: {*final_result}"); } else { print("Calculation failed due to division by zero."); }}
What It Does:
- Chains multiple operations while handling errors with
std::Error
. - Uses
*
to extract valid values for further calculations.
fn check_positive(value: int) -> int? { if value <= 0 { panic errors::InvalidArgument("Value must be positive"); } return value;}
fn compute(value: int) -> int? { let checked = check_positive(value);
if checked? { return checked * 2; }
return checked; // propagates the error.}
fn main() { let result = compute(-5);
if errors::InvalidArgument in result { print("Error: Value must be positive."); } else if result? { print(f"Computation result: {result}"); }}
What It Does:
- Propagates errors early using
panic
in helper functions. - Prevents invalid values from progressing through computations.
Best Practices for Panic
Best Practices for PanicCommon Mistakes
Common MistakesForgetting Explicit Value Extraction
Forgetting Explicit Value Extractionfn divide(a: int, b: int) -> std::Error::<int, errors::DivideByZero> { if b == 0: panic errors::DivideByZero("Cannot divide by zero");
return a / b;}
fn main() { let res = divide(10, 2); let value = res; // Error: Cannot assign without dereferencing. Since res is // a std::Error::<int, errors::DivideByZero> and not a // questionable type.
let correct_value = *res; // Correct way to extract the value.}
Skipping Validity Checks
Skipping Validity Checksfn faulty_function() -> int?: panic errors::Unexpected("This will fail");
fn main() { let result = faulty_function(); // no crash here but the program will crash if you try to use result. print(result); // Error: Unexpected: "This will fail" (crash)}
Conclusion
ConclusionThe panic
statement in Helix is a powerful and integrated error-handling mechanism. By leveraging questionable types (?
) or std::Error::<T, E...>
, you can create predictable and robust error propagation paths.
Ensure every panic
is handled gracefully with checks (...?
, error in ..
, .has_value()
) and explicitly extract values where needed. This design keeps Helix code clean, safe, and predictable.