Error handling is the core of robust software. Topaz provides Result and Option types for type-safe error handling. 🛡️
Result-first philosophy
Prefer Result with ?/try in Topaz v4. throw remains only as interoperability sugar for external APIs.
🎯 Result Type
Basic Result Usage
// Result<SuccessType, ErrorType>
function divide(numerator: int, denominator: int) -> Result<float, string> {
if denominator == 0 {
return Err("Cannot divide by zero")
}
return Ok(numerator / denominator)
}
// Handling Result
let result = divide(10, 2)
match result {
case Ok(value) => print("Result: {value}")
case Err(errorMessage) => print("Error: {errorMessage}")
}
// Various error cases
function readFile(path: string) -> Result<string, string> {
if path.length() == 0 {
return Err("File path is empty")
}
if !path.endsWith(".txt") {
return Err("Only text files are supported")
}
if !fileExists(path) {
return Err("File not found: {path}")
}
// Actual file reading logic
return Ok("File content")
}
Result Chaining
// Transform success value with map
let result = divide(10, 2)
.map(value => value * 2)
.map(value => "Result: {value}")
match result {
case Ok(message) => print(message) // "Result: 10"
case Err(error) => print("Error: {error}")
}
// Chain processing with andThen
function squareRoot(value: float) -> Result<float, string> {
if value < 0 {
return Err("Cannot calculate square root of negative number")
}
return Ok(Math.sqrt(value))
}
let finalResult = divide(100, 4)
.andThen(value => squareRoot(value))
.map(value => "Square root: {value}")
print(finalResult) // Ok("Square root: 5")
// Error recovery with orElse
function useDefault(error: string) -> Result<float, string> {
print("Using default value: {error}")
return Ok(0.0)
}
let recoveredResult = divide(10, 0)
.orElse(useDefault)
print(recoveredResult) // Ok(0.0)
Complex Error Types
// Structured error types
enum FileError {
NotFound(path: string),
PermissionDenied(path: string),
ReadFailed(reason: string),
FormatError(expected: string, actual: string)
}
function readConfigFile(path: string) -> Result<{ [string]: any }, FileError> {
if !fileExists(path) {
return Err(FileError.NotFound(path: path))
}
if !hasReadPermission(path) {
return Err(FileError.PermissionDenied(path: path))
}
match readFileContent(path) {
case Ok(content) => {
match JSON.parse(content) {
case Ok(config) => return Ok(config)
case Err(_) => return Err(FileError.FormatError(
expected: "JSON",
actual: "Invalid format"
))
}
}
case Err(reason) => return Err(FileError.ReadFailed(reason: reason))
}
}
// Specific error handling
match readConfigFile("config.json") {
case Ok(config) => {
print("Config loaded successfully")
print("Theme: {config.theme}")
}
case Err(FileError.NotFound(path: path)) => {
print("Config file not found: {path}")
createDefaultConfig()
}
case Err(FileError.PermissionDenied(path: path)) => {
print("No permission to read file: {path}")
}
case Err(FileError.FormatError(expected: expected, actual: actual)) => {
print("Format error - Expected: {expected}, Actual: {actual}")
}
case Err(FileError.ReadFailed(reason: reason)) => {
print("File read failed: {reason}")
}
}
🔍 Option Type
Basic Option Usage
// Option<T> - value may or may not exist
function findUser(id: int) -> Option<{ name: string, age: int }> {
let users = [
{ id: 1, name: "TopazDev", age: 25 },
{ id: 2, name: "CodeMaster", age: 30 }
]
for user in users {
if user.id == id {
return Some({ name: user.name, age: user.age })
}
}
return None
}
// Handling Option
match findUser(1) {
case Some(user) => print("User found: {user.name}, {user.age} years old")
case None => print("User not found")
}
// Finding element in array
function firstElement<T>(array: [T]) -> Option<T> {
if array.length() > 0 {
return Some(array[0])
}
return None
}
let numbers = [1, 2, 3, 4, 5]
match firstElement(numbers) {
case Some(first) => print("First element: {first}")
case None => print("Array is empty")
}
Option Method Chaining
// Transform value with map
let result = findUser(1)
.map(user => user.name.toUpperCase())
.map(name => "Hello, {name}!")
match result {
case Some(greeting) => print(greeting)
case None => print("User not found")
}
// Chain Options with andThen
function checkAdult(user: { name: string, age: int }) -> Option<string> {
if user.age >= 18 {
return Some(user.name)
}
return None
}
let adultUser = findUser(1)
.andThen(checkAdult)
.map(name => "{name} is an adult")
// Provide default value with unwrapOr
let userName = findUser(999)
.map(user => user.name)
.unwrapOr("Unknown User")
print("User name: {userName}") // "User name: Unknown User"
// Filter with condition
let youngUser = findUser(1)
.filter(user => user.age < 30)
.map(user => "{user.name} is young")
Combining Option and Result
// Convert Option to Result
function requireUser(id: int) -> Result<{ name: string, age: int }, string> {
return findUser(id)
.okOr("User not found: ID {id}")
}
// Convert Result to Option
let optionalResult = divide(10, 0).ok() // Ok becomes Some, Error becomes None
match optionalResult {
case Some(value) => print("Division successful: {value}")
case None => print("Division failed (error message ignored)")
}
// Complex processing
function doubleUserAge(id: int) -> Result<int, string> {
return requireUser(id)
.map(user => user.age * 2)
}
match doubleUserAge(1) {
case Ok(doubleAge) => print("Double age: {doubleAge}")
case Err(error) => print("Error: {error}")
}
🔗 Error Propagation and Early Return
? Operator (Error Propagation)
// Automatic error propagation with ? operator
function complexCalculation(a: int, b: int, c: int) -> Result<float, string> {
let first = divide(a, b)? // Auto return on error
let second = squareRoot(first)? // Auto return on error
let final = divide(second, c)? // Auto return on error
return Ok(final)
}
// Above code is equivalent to:
function complexCalculation_manual(a: int, b: int, c: int) -> Result<float, string> {
let first = match divide(a, b) {
case Ok(value) => value
case Err(error) => return Err(error)
}
let second = match squareRoot(first) {
case Ok(value) => value
case Err(error) => return Err(error)
}
let final = match divide(second, c) {
case Ok(value) => value
case Err(error) => return Err(error)
}
return Ok(final)
}
// ? operator can also be used with Option
function firstAndSecondSum(array: [int]) -> Option<int> {
let first = firstElement(array)?
let second = firstElement(array.slice(1))?
return Some(first + second)
}
try Block (Experimental)
// Group multiple operations with try block
function processData(filePath: string) -> Result<{ total: int, average: float }, string> {
try {
let content = readFile(filePath)?
let numbers = content.split(",")
.map(s => parseInt(s.trim()))
.collect()
let total = numbers.sum()
let average = divide(total, numbers.length())?
Ok({ total: total, average: average })
}
}
// Nested try processing
function nestedProcessing() -> Result<string, string> {
let result1 = try {
let a = divide(10, 2)?
let b = squareRoot(a)?
Ok(b)
}?
let result2 = try {
let c = divide(result1, 3)?
Ok(c * 2)
}?
return Ok("Final result: {result2}")
}
defer (Scope Guards)
v4
Register cleanup actions to run when the current scope exits. Execution order is LIFO. defer runs on all exit paths: normal completion, return, early return via ?, and errors.
Policy: errors thrown inside a defer block are logged and not propagated by default; all remaining deferred actions still run.
function processFile(path: string) -> Result<string, string> {
let file = File.open(path)?
defer { file.close() } // always closes (LIFO)
let lock = file.lockExclusive()? // acquire resource
defer { lock.release() } // released even on error/return
let content = file.read()? // work with the resource
Ok(content)
}
🎨 Error Handling Patterns
Adding Error Context
// Add context information to errors
function loadUserData(id: int) -> Result<{ name: string, settings: any }, string> {
let user = requireUser(id)
.mapErr(error => "User lookup failed (ID: {id}): {error}")?
let settings = readConfigFile("user_{id}.json")
.mapErr(error => "User settings load failed (ID: {id}): {error}")?
return Ok({ name: user.name, settings: settings })
}
// Step-by-step error handling
function stepByStepProcessing() -> Result<string, string> {
print("Step 1: Validating data...")
let validationResult = validateData().mapErr(error => "Step 1 failed: {error}")?
print("Step 2: Transforming data...")
let transformResult = transformData(validationResult).mapErr(error => "Step 2 failed: {error}")?
print("Step 3: Saving data...")
let saveResult = saveData(transformResult).mapErr(error => "Step 3 failed: {error}")?
return Ok("All steps completed: {saveResult}")
}
Partial Failure Handling
// Allow partial failures in multiple operations
function batchProcess(fileList: [string]) -> { success: [string], failures: [{ file: string, error: string }] } {
let mut successList = []
let mut failureList = []
for file in fileList {
match processFile(file) {
case Ok(result) => {
successList.push(result)
print("Success: {file}")
}
case Err(error) => {
failureList.push({ file: file, error: error })
print("Failed: {file} - {error}")
}
}
}
return { success: successList, failures: failureList }
}
// Processing Result arrays
function collectAllResults<T, E>(results: [Result<T, E>]) -> Result<[T], [E]> {
let mut successValues = []
let mut errors = []
for result in results {
match result {
case Ok(value) => successValues.push(value)
case Err(error) => errors.push(error)
}
}
if errors.length() > 0 {
return Err(errors)
}
return Ok(successValues)
}
let calculationResults = [
divide(10, 2), // Ok(5)
divide(8, 4), // Ok(2)
divide(6, 0) // Err("Cannot divide by zero")
]
match collectAllResults(calculationResults) {
case Ok(values) => print("All calculations successful: {values}")
case Err(errors) => print("Some calculations failed: {errors}")
}
Retry Pattern
// Retry logic
function retry<T, E>(
operation: function() -> Result<T, E>,
maxAttempts: int,
delay: int = 1000
) -> Result<T, E> {
let mut attempts = 0
while attempts < maxAttempts {
attempts += 1
match operation() {
case Ok(result) => {
if attempts > 1 {
print("Succeeded on retry attempt {attempts}")
}
return Ok(result)
}
case Err(error) => {
print("Attempt {attempts} failed: {error}")
if attempts < maxAttempts {
print("Retrying after {delay}ms...")
Thread.sleep(delay)
} else {
return Err(error)
}
}
}
}
return Err("Maximum retry attempts exceeded")
}
// Network request retry
let response = retry(
operation: () => networkRequest("https://api.example.com/data"),
maxAttempts: 3,
delay: 2000
)
match response {
case Ok(data) => print("Data received: {data}")
case Err(error) => print("Final failure: {error}")
}
🛡️ Type Safety and Compile-time Checking
Forcing All Error Cases to be Handled
// Compiler forces handling of all cases
enum NetworkError {
ConnectionFailed,
Timeout,
AuthenticationFailed,
ServerError(code: int)
}
function handleNetworkRequest(result: Result<string, NetworkError>) -> string {
match result {
case Ok(data) => "Success: {data}"
case Err(NetworkError.ConnectionFailed) => "Cannot connect to network"
case Err(NetworkError.Timeout) => "Request timed out"
case Err(NetworkError.AuthenticationFailed) => "Authentication required"
case Err(NetworkError.ServerError(code: code)) => "Server error (code: {code})"
// Compile error if not all cases are handled!
}
}
// Never type for impossible situations
function neverFails() -> Result<string, Never> {
return Ok("Always succeeds")
}
// Never doesn't need to be matched
let result = neverFails()
let value = match result {
case Ok(value) => value
// Error case is impossible, so can be omitted
}
Custom Error Traits
// Trait to enrich error information
trait ErrorInfo {
function message() -> string
function code() -> int
function recoverable() -> bool
}
enum ApplicationError: ErrorInfo {
DatabaseConnectionFailed,
FileAccessDenied,
InvalidInput(field: string, value: string),
ExternalServiceUnavailable
}
impl ErrorInfo for ApplicationError {
function message() -> string {
match self {
case ApplicationError.DatabaseConnectionFailed => "Cannot connect to database"
case ApplicationError.FileAccessDenied => "No permission to access file"
case ApplicationError.InvalidInput(field: field, value: value) => "Invalid input - {field}: {value}"
case ApplicationError.ExternalServiceUnavailable => "External service unavailable"
}
}
function code() -> int {
match self {
case ApplicationError.DatabaseConnectionFailed => 1001
case ApplicationError.FileAccessDenied => 1002
case ApplicationError.InvalidInput(_, _) => 1003
case ApplicationError.ExternalServiceUnavailable => 1004
}
}
function recoverable() -> bool {
match self {
case ApplicationError.DatabaseConnectionFailed => true
case ApplicationError.ExternalServiceUnavailable => true
case _ => false
}
}
}
// Integrated error handling
function logErr(error: ApplicationError) {
print("[Error {error.code()}] {error.message()}")
if error.recoverable() {
print("Recovery can be attempted")
} else {
print("Unrecoverable error")
}
}
🎯 Optimization and Best Practices
1. Treat Errors as Values
// Good: Explicitly handle errors
function safeUserInput() -> Result<int, string> {
let input = input("Enter a number: ")
match parseInt(input) {
case Ok(number) if number >= 0 => Ok(number)
case Ok(_) => Err("Negative numbers not allowed")
case Err(_) => Err("Not a valid number: {input}")
}
}
// Bad: Throwing exceptions (discouraged in Topaz)
function dangerousUserInput() -> int {
let input = input("Enter a number: ")
let number = parseInt(input).unwrap() // Can panic!
return number
}
2. Optimize Error Propagation
// Efficient error propagation
function dataPipeline(input: string) -> Result<string, string> {
input
.trim()
.nonEmpty().okOr("Input is empty")?
.validate().mapErr(error => "Validation failed: {error}")?
.transform().mapErr(error => "Transformation failed: {error}")?
.process().mapErr(error => "Processing failed: {error}")
}
// Prevent deep nesting with early returns
function registerUser(data: { name: string, email: string, age: int }) -> Result<string, string> {
// Early validation
if data.name.length() == 0 {
return Err("Name is required")
}
if !data.email.contains("@") {
return Err("Valid email is required")
}
if data.age < 0 {
return Err("Valid age is required")
}
// Process only after all validations pass
let user = createUser(data)?
let id = saveToDatabase(user)?
return Ok("User registration complete: {id}")
}
3. Error Logging and Monitoring
// Structured error logging
function handleWithLogging<T, E>(
result: Result<T, E>,
context: string,
logLevel: string = "error"
) -> Result<T, E> {
match result {
case Ok(value) => {
if logLevel == "debug" {
print("[DEBUG] {context}: Success")
}
return Ok(value)
}
case Err(error) => {
print("[{logLevel.toUpperCase()}] {context}: {error}")
// Collect metrics
incrementErrorMetric(context, logLevel)
return Err(error)
}
}
}
// Usage example
let result = databaseQuery("SELECT * FROM users")
|> handleWithLogging(context: "User list query", logLevel: "error")
// Error aggregation and alerting
function errorAlertSystem() {
let recentErrors = collectErrorLogs(lastHours: 1) // 1 hour
if recentErrors.length() > 100 {
sendAlert("High error rate detected: {recentErrors.length()} errors/hour")
}
let criticalErrors = recentErrors.filter(error => error.level == "critical")
if criticalErrors.length() > 0 {
sendUrgentAlert("Critical errors occurred", criticalErrors)
}
}
Topaz error handling is type-safe and explicit. Use Result and Option to build predictable and robust applications! 🚀