ESC
Type to search...
S
Soli Docs

Error Handling

Try/catch/finally for exception handling, throw for raising errors.

Try / Catch / Finally

try / catch / finally

Handles exceptions with try/catch blocks, using end-delimited syntax just like if, while, and for. Ruby-style aliases are supported: begin for try, and rescue for catch.

# Basic try/catch
try
  result = 10 / 0
catch e
  print("Error: " + str(e))
end

# With finally (always runs)
try
  data = read_file("config.sl")
  print(data)
catch e
  print("Failed: " + str(e))
finally
  print("Cleanup done")
end

# Try/finally without catch
try
  process_data()
finally
  close_connection()
end

# Ruby-style aliases: `begin` for `try`, `rescue` for `catch`
begin
  risky_operation()
rescue e
  print("Error: " + str(e))
end

Catch Variable Syntax

The catch variable can be written with or without parentheses:

# Without parentheses
try
  risky()
catch e
  print(e)
end

# With parentheses
try
  risky()
catch (e)
  print(e)
end

Throwing Exceptions

throw

Raises an exception that can be caught by a surrounding try/catch block.

def divide(a: Int, b: Int) -> Int
  if b == 0
    throw "Division by zero"
  end
  a / b
end

try
  result = divide(10, 0)
catch e
  print("Caught: " + str(e))  # "Caught: Division by zero"
end

End Syntax

Try/catch supports end-delimited blocks:

try
  result = risky_operation()
catch e
  print("Error: " + str(e))
finally
  cleanup()
end

Typed Catch

catch ClassName e

Catch specific error types by class name. Multiple catch blocks are tried in order. A bare catch without a type acts as a catch-all.

# Define error classes
class NotFoundError
  message: String
  new(msg: String)
    this.message = msg
  end
end

class ValidationError
  message: String
  new(msg: String)
    this.message = msg
  end
end

# Multiple typed catch blocks
try
  throw new NotFoundError("User not found")
catch NotFoundError e
  print("404: " + e.message)
catch ValidationError e
  print("Invalid: " + e.message)
catch e
  print("Unknown error: " + str(e))
end
Catching by Superclass

Typed catches walk the inheritance chain. A catch BaseError will also catch any subclass of BaseError.

class AppError
  message: String
  new(msg: String)
    this.message = msg
  end
end

class NotFoundError < AppError
  new(msg: String)
    super(msg)
  end
end

class PermissionError < AppError
  new(msg: String)
    super(msg)
  end
end

# Catches NotFoundError because it extends AppError
try
  throw new NotFoundError("Page not found")
catch AppError e
  print("App error: " + e.message)
end
Unmatched Exceptions

If no typed catch matches, the exception is re-thrown to the next outer try/catch. Add a bare catch at the end to handle all remaining exceptions.

class ErrorA {}
class ErrorB {}

try
  try
    throw new ErrorB()
  catch ErrorA e
    # Does NOT match ErrorB
    print("A")
  end
catch e
  # ErrorB bubbles up here
  print("Caught in outer: " + str(e))
end

Typed Catch Rules

  • Typed catches only match class instances (strings, ints, etc. will not match)
  • Catches are tried in order — put more specific types first
  • A bare catch e catches everything (including non-instance values)
  • Subclasses match their parent class catches (walks the inheritance chain)
  • Works with both end and {} syntax

Nested Try/Catch

Try/catch blocks can be nested for fine-grained error handling:

try
  print("Outer try")
  try
    throw "inner error"
  catch e
    print("Inner catch: " + str(e))
  end
  print("After inner try")
catch e
  print("Outer catch: " + str(e))
end

Postfix Rescue

expr rescue fallback

Returns fallback if expr throws an exception. A concise alternative to try/catch for simple cases.

# Simple rescue
data = fetch_data() rescue null;
print(data);  # null if fetch_data threw

# With fallback value
config = load_config() rescue {"host": "localhost", "port": 8080};

# Chaining with nullish coalescing
result = risky_operation() rescue null ?? "default";

# In pipelines
data = user_id |> fetch_user |> validate_user rescue null;
Precedence

Rescue has the same precedence as assignment, so x = y rescue z parses as x = (y rescue z).

# These are equivalent
x = risky() rescue "default";
x = (risky() rescue "default");

# Can chain with other operators
a = throw "err" rescue "x" or "y";  # ("x" or "y") is the fallback

When to Use Postfix Rescue

  • Use for simple fallbacks where you just need a default value
  • Use try/catch when you need to handle the error, run cleanup code, or make decisions based on the error type
  • Rescue swallows the error - if you need to log or re-throw, use try/catch instead