Error Handling

Master Topaz powerful error handling system. Learn safe error handling approaches using Result types, Option types, pattern matching, and exception handling optimization strategies.

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! 🚀