Exception Handling #
Quest provides exception handling for error management and recovery using try, catch, ensure, and raise keywords.
Basic Exception Handling #
Try-Catch Block #
try
# Code that might raise an exception
let result = risky_operation()
puts("Success: ", result)
catch e
# Handle the exception
puts("Error occurred: ", e.message)
end
Try-Catch-Ensure #
The ensure block always executes, whether an exception occurs or not:
try
file = io.open("data.txt", "r")
let content = file.read()
process(content)
catch e
puts("Failed to read file: ", e.message)
ensure
# Always runs, even if exception occurs
if file != nil
file.close()
end
end
Raising Exceptions #
Basic Raise #
if value < 0
raise "Value cannot be negative"
end
Raise with Exception Type #
if !user.is_authenticated()
raise AuthenticationError("User not logged in")
end
if file_size > max_size
raise ValueError("File too large: " .. file_size .. " bytes")
end
Re-raising Exceptions #
try
dangerous_operation()
catch e
puts("Logging error: ", e.message)
raise # Re-raise the same exception
end
Multiple Catch Blocks #
Handle different exception types differently using type annotations:
try
let data = json.parse(user_input)
process_data(data)
catch e: ValueErr
puts("Invalid JSON format: " .. e.message())
return nil
catch e: TypeErr
puts("Type error: " .. e.message())
return nil
catch e: IOErr
puts("I/O error: " .. e.message())
return nil
catch e: Err
# Catch all other exception types
puts("Unexpected error: " .. e.message())
raise # Re-raise if we don't know how to handle it
end
The order matters: catch specific exception types first, then more general ones.
Exception Object #
Exception objects have the following properties:
try
risky_function()
catch e
puts("Type: ", e.type) # Exception type name
puts("Message: ", e.message) # Error message
puts("Stack: ", e.stack) # Stack trace
puts("Line: ", e.line) # Line number where error occurred
puts("File: ", e.file) # File where error occurred
end
Built-in Exception Types #
Quest provides a hierarchical exception system where all exception types implement the Error trait. This enables both specific and generic error handling through type-based catch clauses.
Standard Exception Types #
All built-in exceptions implement the Error trait and can be caught using type annotations:
- Err - Base exception type (catches all exceptions)
- IndexErr - Sequence index out of range
- TypeErr - Wrong type for operation
- ValueErr - Invalid value for operation
- ArgErr - Wrong number or type of arguments
- AttrErr - Object has no attribute or method
- NameErr - Name not found in scope
- RuntimeErr - Generic runtime error
- IOErr - Input/output operation failed
- ImportErr - Module import failed
- KeyErr - Dictionary key not found
Creating Typed Exceptions #
Use the .new() method to create exception instances:
raise Err.new("Generic error message")
raise IndexErr.new("index out of bounds: 10")
raise TypeErr.new("expected str, got int")
raise ValueErr.new("value must be positive")
raise ArgErr.new("expected 2 arguments, got 3")
raise AttrErr.new("object has no attribute 'foo'")
raise NameErr.new("name 'x' is not defined")
raise RuntimeErr.new("something went wrong")
raise IOErr.new("failed to read file")
raise ImportErr.new("module 'foo' not found")
raise KeyErr.new("key 'config' not found")
Hierarchical Exception Catching #
Catch specific exception types or use the base Err type to catch all:
try
let item = array[10] # May raise IndexErr
catch e: IndexErr
puts("Index error: " .. e.message())
catch e: Err
# Catches all other exception types
puts("Other error: " .. e.type())
end
Exception Object Methods #
All exception objects provide these methods:
try
risky_operation()
catch e
puts(e.type()) # Exception type name (e.g., "IndexErr")
puts(e.message()) # Error message
puts(e.stack()) # Stack trace
puts(e.str()) # String representation
end
Backwards Compatibility #
String-based exceptions are still supported and are treated as RuntimeErr:
raise "Something went wrong" # Equivalent to RuntimeErr.new("Something went wrong")
Custom Exceptions #
Define custom exception types:
type ValidationError
message: Str
field: Str
end
fun validate_email(email)
if !email.contains("@")
raise ValidationError("Invalid email format", email)
end
end
try
validate_email(user_input)
catch e: ValidationError
puts("Field '", e.field, "' is invalid: ", e.message)
end
Exception Chaining #
Preserve the original exception when raising a new one:
try
let data = load_data()
process_data(data)
catch e: FileNotFoundError
# Chain the original exception
raise ProcessingError("Failed to process data", e)
end
# Later, you can inspect the chain:
catch e: ProcessingError
puts("Error: ", e.message)
puts("Caused by: ", e.cause.message)
end
Pattern: Resource Management #
Ensure resources are cleaned up:
fun read_file_safely(path)
let file = nil
try
file = io.open(path, "r")
return file.read()
catch e: FileNotFoundError
puts("File not found: ", path)
return nil
catch e: PermissionError
puts("Permission denied: ", path)
return nil
ensure
if file != nil
file.close()
end
end
end
Pattern: Validation with Exceptions #
fun validate_user_data(data)
if data == nil
raise ValueError("User data cannot be nil")
end
if !data.contains("email")
raise ValidationError("Missing required field: email")
end
if !data.contains("name")
raise ValidationError("Missing required field: name")
end
if data.email.len() < 5
raise ValidationError("Email too short")
end
return true
end
try
validate_user_data(user_input)
save_user(user_input)
puts("User saved successfully")
catch e: ValidationError
puts("Validation failed: ", e.message)
show_error_to_user(e.message)
catch e
puts("Unexpected error: ", e.message)
log_error(e)
end
Pattern: Graceful Degradation #
fun fetch_user_avatar(user_id)
try
return api.get_avatar(user_id)
catch e: NetworkError
puts("Network error, using default avatar")
return default_avatar
catch e: TimeoutError
puts("Request timed out, using cached avatar")
return cache.get_avatar(user_id, default_avatar)
end
end
Pattern: Transaction Rollback #
fun transfer_funds(from_account, to_account, amount)
let transaction = db.begin_transaction()
try
from_account.withdraw(amount)
to_account.deposit(amount)
transaction.commit()
puts("Transfer successful")
catch e
transaction.rollback()
puts("Transfer failed, rolled back: ", e.message)
raise
ensure
transaction.close()
end
end
Pattern: Logging and Re-raising #
fun critical_operation()
try
perform_operation()
catch e
# Log the error
logger.error("Critical operation failed", {
"error": e.message,
"type": e.type,
"stack": e.stack
})
# Send alert
alerts.send_critical_error(e)
# Re-raise to let caller handle it
raise
end
end
Pattern: Timeout Protection #
fun fetch_with_timeout(url, timeout_seconds)
let timer = time.start_timer(timeout_seconds)
try
return http.get(url)
catch e: TimeoutError
puts("Request timed out after ", timeout_seconds, " seconds")
return nil
ensure
timer.cancel()
end
end
Best Practices #
1. Be Specific with Exception Types #
# Bad: Too generic
try
process_data()
catch e
# What went wrong?
puts("Error")
end
# Good: Handle specific cases
try
process_data()
catch e: ValidationError
handle_validation_error(e)
catch e: NetworkError
handle_network_error(e)
catch e: FileNotFoundError
handle_missing_file(e)
catch e
# Only catch unexpected errors here
log_unexpected_error(e)
raise
end
2. Don't Swallow Exceptions Silently #
# Bad: Silent failure
try
important_operation()
catch e
# Nothing - error disappears
end
# Good: At least log it
try
important_operation()
catch e
logger.error("Operation failed: ", e.message)
# Or provide fallback behavior
end
3. Clean Up Resources #
# Always use ensure for cleanup
try
connection = db.connect()
perform_queries(connection)
catch e
puts("Database error: ", e.message)
ensure
if connection != nil
connection.close()
end
end
4. Provide Context in Error Messages #
# Bad: Vague message
raise "Invalid input"
# Good: Specific and actionable
raise ValueError("Email must contain '@' symbol, got: " .. email)
5. Use Exceptions for Exceptional Cases #
# Bad: Using exceptions for control flow
try
find_user(id)
catch e: NotFoundError
# Expected case, shouldn't use exception
create_user(id)
end
# Good: Use return values for expected cases
let user = find_user(id)
if user == nil
user = create_user(id)
end