Error Handling
Implementation status: do / catch / throw / expression try / try? / try! / Result<T, E> Implemented (SA-2-01); throws T typed throws Partially implemented; rethrows Not implemented
SYX provides three error-propagation paths:
- throws / do-catch / try expr — raising and catching exceptions (runtime errors)
- Result<T, E> — an explicit algebraic type
- Optional — using
nilto mean failure (the lightest weight)
This chapter covers syntax, usage, Result integration, and cross-function propagation. Charter: sa-2-01-do-catch.
do / catch Blocks
import System
func main() {
do {
let content = File.read("/tmp/x")
print(content)
} catch (err) {
print("failed: \(err)")
}
}- Inside a
do { }block you maythrow/ callthrowsfunctions - When an exception fires, control jumps immediately to
catch, skipping the rest of thedoblock - The MVP type of
errisString(more complex error types are planned; SA-2-02)
Source: crates/syx-driver/tests/fixtures/lsp/result_generic.sy
Cross-function try (R-38 output pin · SEM-THROW)
do { try g() } catch must enter the catch path (the error must not print as a normal value):
// prints: caught
enum E: Error {
case bad
}
func g() throws {
throw E.bad
}
func main() {
do {
try g()
print("value")
} catch {
print("caught")
}
}throw Expressions
func divide(_ a: Int, _ b: Int) throws -> Int {
if (b == 0) {
throw "division by zero"
}
return a / b
}throw exprthrowsexpr(MVP typeString)- Legal only inside a
throwsfunction - Throwing a
String/Result.err/ a custom enum is all allowed (the type ofexpris determined by the function signature'sthrows T)
throws Function Declarations
import System
func loadFile(path: String) throws -> String {
if (!File.exists(path)) {
throw "not found: \(path)"
}
return File.read(path)
}
func parseInt(s: String) throws -> Int {
if (s.count == 0) {
throw "empty"
}
return 0
}| Signature | Meaning |
|---|---|
throws -> T | May throw; callers need try |
throws E -> T | Typed throws (Partially implemented), throws E |
async throws -> T | async and throws |
try Expressions
Three forms for calling throws functions:
try expr (try is mandatory)
import System
func loadFile(path: String) throws -> String {
if (!File.exists(path)) { throw "not found" }
return File.read(path)
}
func main() {
let s = try loadFile(path: "/tmp/x") // String
print(s)
}- It must be wrapped in
try(the compiler reports missing wrapping as P001) - Equivalent to
do { let s = loadFile(...) }
try? expr (failure becomes nil)
import System
func loadFile(path: String) throws -> String {
if (!File.exists(path)) { throw "not found" }
return File.read(path)
}
func main() {
let s = try? loadFile(path: "/tmp/x") // String?
print(s)
}- Success → returns
T - Failure → returns
nil(the exception is swallowed)
try! expr (forced non-erroring)
import System
func loadFile(path: String) throws -> String {
if (!File.exists(path)) { throw "not found" }
return File.read(path)
}
func main() {
let s = try! loadFile(path: "/tmp/x") // String
print(s)
}- Success → returns
T - Failure → runtime panic
try?chains: severaltry?s may be chained:try? foo() + try? bar(), and any failure makes the whole thingnil(depending on the exact semantics).
Chained catch
do {
let r = Http.get(url)
print(r)
} catch (err) where (err.hasPrefix("5")) {
print("server error: \(err)")
} catch (err) where (err.hasPrefix("4")) {
print("client error: \(err)")
} catch (err) {
print("other: \(err)")
}
whereguards: early versions supportedwhereguards on try-catch blocks (Partially implemented); callingtry?/ switching on a Result directly is recommended.
The Result Type
Result<T, E> is the standard solution for explicit error types (Chapter 9, Enumerations):
func parse(s: String) -> Result<String, String> {
if (s.isEmpty) { return Result.err(value: "empty") }
return Result.ok(value: s)
}
func main() {
let r = parse(s: "hello")
if (Result.isOk(result: r)) {
print("ok")
} else {
print("err")
}
}Static Result Construction
Result.ok(value:) / Result.err(value:) are the String-typed MVP constructors (packages/core/builtins/algebraic/result.sy):
func main() {
let r1 = Result.ok(value: "42") // Result<String, String> success
let r2 = Result.err(value: "oops") // Result<String, String> failure
print(Result.isOk(result: r1))
print(Result.isErr(result: r2))
}Result Chaining
Thin MVP check surface (full .map / .flatMap generics still deepen later):
func main() {
let r1 = Result.ok(value: "10")
print(Result.isOk(result: r1)) // true
let r2 = Result.err(value: "neg")
print(Result.isErr(result: r2)) // true
}Methods:
Result.ok/Result.err/Result.isOk/Result.isErr/.descriptionlive inpackages/core/builtins/algebraic/result.sy. Generic.map/.flatMapremain a deepen track.
@Codable + Result Serialization
@Codable
enum ApiError: Error {
case notFound
case serverError(Int, String)
}Source: fixtures/lsp/result_generic.sy · fixtures/lsp/result_generic.sy
throws and Actors
A throws func inside an actor is isolated automatically:
import System
actor FileManager {
func read(path: String) throws -> String {
if (!File.exists(path)) { throw "not found" }
return File.read(path)
}
}
async func main() {
let m = FileManager()
do {
let s = await m.read(path: "/tmp/x")
print(s)
} catch (err) {
print("failed: \(err)")
}
}Choosing an Error-Handling Strategy
| Scenario | Choose |
|---|---|
| Runtime errors that must be handled (IO, network) | throws + try / catch |
| Recoverable logic errors (parsing, conversion) | Result<T, E> |
| A two-state "present/absent" (lookup, cache hit) | Optional<T> |
| Internal panic paths in libraries | try! / fatalError |
| Error types carrying multiple fields | enum + Result / typed throws |
Complete Example
enum MathError {
case divideByZero
case overflow
}
func safeDivide(_ a: Int, _ b: Int) -> Result<Int, MathError> {
if (b == 0) { return .err(.divideByZero) }
if (a > 1000) { return .err(.overflow) }
return .ok(a / b)
}
func doubleAndPrint(_ a: Int, _ b: Int) {
switch (safeDivide(a, b)) {
case .ok(let n) {
print("\(a) / \(b) = \(n * 2)")
}
case .err(.divideByZero) {
print("oops: divide by zero")
}
case .err(.overflow) {
print("oops: overflow")
}
}
}
func main() {
doubleAndPrint(10, 2) // "10 / 2 = 10"
doubleAndPrint(10, 0) // "oops: divide by zero"
doubleAndPrint(2000, 5) // "oops: overflow"
}Comparison with the Swift Style
| Swift | SYX |
|---|---|
do { try … } catch { } | do { } catch { } (no do) |
The Error protocol | The MVP uses String / Result |
throws E typed throws | Partially implemented |
try? try! | Implemented |
rethrows | Not implemented |
stdlib Result<T, E> | Implemented (including static ok / err) |
error: { } closure parameter | A catch (err) { } block |
try? foo()?.bar() | Legal |
Diagnostics
| Code | Trigger | Fix |
|---|---|---|
| S020 | throw inside a non-throws function | Add throws |
| P001 | Calling a throws function without try | Add try / try? / try! |
| R007 | try! failed | Change to try? / do { } catch |
| S004 | throws type mismatch | Fix throws T |
Next chapter → Concurrency
See also → Language Reference: Statements · Language Reference: Declarations · Chapter 9, Enumerations · Chapter 18, Concurrency