docs: add comprehensive language documentation
Documentation structure inspired by Rust Book, Elm Guide, and others: Guide (10 chapters): - Introduction and setup - Basic types (Int, String, Bool, List, Option, Result) - Functions (closures, higher-order, composition) - Data types (ADTs, pattern matching, records) - Effects (the core innovation) - Handlers (patterns and techniques) - Modules (imports, exports, organization) - Error handling (Fail, Option, Result) - Standard library reference - Advanced topics (traits, generics, optimization) Reference: - Complete syntax reference Tutorials: - Calculator (parsing, evaluation, REPL) - Dependency injection (testing with effects) - Project ideas (16 projects by difficulty) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
294
docs/guide/08-errors.md
Normal file
294
docs/guide/08-errors.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# Chapter 8: Error Handling
|
||||
|
||||
Errors happen. Lux provides multiple ways to handle them, all explicit in the type system.
|
||||
|
||||
## The Fail Effect
|
||||
|
||||
The simplest error handling uses the built-in `Fail` effect:
|
||||
|
||||
```lux
|
||||
fn divide(a: Int, b: Int): Int with {Fail} =
|
||||
if b == 0 then Fail.fail("Division by zero")
|
||||
else a / b
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let result = run divide(10, 2) with {}
|
||||
Console.print("Result: " + toString(result))
|
||||
}
|
||||
```
|
||||
|
||||
When `b == 0`, `Fail.fail` stops execution. The program terminates with an error.
|
||||
|
||||
### Fail Propagates
|
||||
|
||||
```lux
|
||||
fn helper(): Int with {Fail} =
|
||||
Fail.fail("Oops")
|
||||
|
||||
fn caller(): Int with {Fail} = {
|
||||
let x = helper() // Fails here
|
||||
x * 2 // Never reached
|
||||
}
|
||||
```
|
||||
|
||||
If you call a function with `Fail`, you must declare it:
|
||||
|
||||
```lux
|
||||
// Error: caller uses Fail but doesn't declare it
|
||||
fn broken(): Int = {
|
||||
divide(10, 0)
|
||||
}
|
||||
```
|
||||
|
||||
## Option<T> - Maybe There's a Value
|
||||
|
||||
For operations that might not have a result:
|
||||
|
||||
```lux
|
||||
fn safeDivide(a: Int, b: Int): Option<Int> =
|
||||
if b == 0 then None
|
||||
else Some(a / b)
|
||||
|
||||
fn findUser(id: Int): Option<User> =
|
||||
// Returns None if user doesn't exist
|
||||
Database.query("SELECT * FROM users WHERE id = " + toString(id))
|
||||
```
|
||||
|
||||
### Working with Option
|
||||
|
||||
**Pattern matching:**
|
||||
```lux
|
||||
fn showResult(opt: Option<Int>): String =
|
||||
match opt {
|
||||
Some(n) => "Got: " + toString(n),
|
||||
None => "No value"
|
||||
}
|
||||
```
|
||||
|
||||
**Option methods:**
|
||||
```lux
|
||||
let x = Some(5)
|
||||
let y: Option<Int> = None
|
||||
|
||||
Option.map(x, fn(n: Int): Int => n * 2) // Some(10)
|
||||
Option.map(y, fn(n: Int): Int => n * 2) // None
|
||||
|
||||
Option.getOrElse(x, 0) // 5
|
||||
Option.getOrElse(y, 0) // 0
|
||||
|
||||
Option.isSome(x) // true
|
||||
Option.isNone(y) // true
|
||||
```
|
||||
|
||||
**Chaining with flatMap:**
|
||||
```lux
|
||||
fn getUserName(id: Int): Option<String> =
|
||||
Option.flatMap(findUser(id), fn(user: User): Option<String> =>
|
||||
Some(user.name)
|
||||
)
|
||||
```
|
||||
|
||||
## Result<T, E> - Success or Failure
|
||||
|
||||
For operations that can fail with an error value:
|
||||
|
||||
```lux
|
||||
fn parseNumber(s: String): Result<Int, String> =
|
||||
if isNumeric(s) then Ok(parseInt(s))
|
||||
else Err("Not a number: " + s)
|
||||
|
||||
fn readConfig(path: String): Result<Config, String> with {File} =
|
||||
if File.exists(path) then
|
||||
match parseConfig(File.read(path)) {
|
||||
Some(c) => Ok(c),
|
||||
None => Err("Invalid config format")
|
||||
}
|
||||
else Err("Config file not found: " + path)
|
||||
```
|
||||
|
||||
### Working with Result
|
||||
|
||||
**Pattern matching:**
|
||||
```lux
|
||||
fn handleResult(r: Result<Int, String>): String =
|
||||
match r {
|
||||
Ok(n) => "Success: " + toString(n),
|
||||
Err(e) => "Error: " + e
|
||||
}
|
||||
```
|
||||
|
||||
**Result methods:**
|
||||
```lux
|
||||
let success = Ok(42)
|
||||
let failure: Result<Int, String> = Err("oops")
|
||||
|
||||
Result.map(success, fn(n: Int): Int => n * 2) // Ok(84)
|
||||
Result.map(failure, fn(n: Int): Int => n * 2) // Err("oops")
|
||||
|
||||
Result.getOrElse(success, 0) // 42
|
||||
Result.getOrElse(failure, 0) // 0
|
||||
|
||||
Result.isOk(success) // true
|
||||
Result.isErr(failure) // true
|
||||
```
|
||||
|
||||
**Chaining operations:**
|
||||
```lux
|
||||
fn processData(input: String): Result<Output, String> = {
|
||||
let parsed = parseInput(input) // Result<Input, String>
|
||||
let validated = Result.flatMap(parsed, validate) // Result<Input, String>
|
||||
Result.map(validated, transform) // Result<Output, String>
|
||||
}
|
||||
```
|
||||
|
||||
## Combining Approaches
|
||||
|
||||
### Option to Result
|
||||
|
||||
```lux
|
||||
fn optionToResult<T>(opt: Option<T>, error: String): Result<T, String> =
|
||||
match opt {
|
||||
Some(v) => Ok(v),
|
||||
None => Err(error)
|
||||
}
|
||||
|
||||
fn findUserOrError(id: Int): Result<User, String> =
|
||||
optionToResult(findUser(id), "User not found: " + toString(id))
|
||||
```
|
||||
|
||||
### Result to Option
|
||||
|
||||
```lux
|
||||
fn resultToOption<T, E>(r: Result<T, E>): Option<T> =
|
||||
match r {
|
||||
Ok(v) => Some(v),
|
||||
Err(_) => None
|
||||
}
|
||||
```
|
||||
|
||||
### Fail with Result
|
||||
|
||||
```lux
|
||||
fn processOrFail(input: String): Output with {Fail} =
|
||||
match process(input) {
|
||||
Ok(output) => output,
|
||||
Err(e) => Fail.fail(e)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### Early Return with Fail
|
||||
|
||||
```lux
|
||||
fn validateUser(user: User): User with {Fail} = {
|
||||
if user.name == "" then Fail.fail("Name required")
|
||||
else if user.age < 0 then Fail.fail("Invalid age")
|
||||
else if user.email == "" then Fail.fail("Email required")
|
||||
else user
|
||||
}
|
||||
|
||||
fn registerUser(user: User): UserId with {Fail, Database} = {
|
||||
let validated = validateUser(user)
|
||||
Database.insert(validated)
|
||||
}
|
||||
```
|
||||
|
||||
### Collecting Errors
|
||||
|
||||
```lux
|
||||
fn validateAll(user: User): Result<User, List<String>> = {
|
||||
let errors: List<String> = []
|
||||
let errors = if user.name == "" then List.concat(errors, ["Name required"]) else errors
|
||||
let errors = if user.age < 0 then List.concat(errors, ["Invalid age"]) else errors
|
||||
let errors = if user.email == "" then List.concat(errors, ["Email required"]) else errors
|
||||
|
||||
if List.isEmpty(errors) then Ok(user)
|
||||
else Err(errors)
|
||||
}
|
||||
```
|
||||
|
||||
### Default Values
|
||||
|
||||
```lux
|
||||
fn getConfig(key: String): String =
|
||||
Option.getOrElse(Config.get(key), "default")
|
||||
|
||||
fn getPort(): Int =
|
||||
Result.getOrElse(parseNumber(Process.env("PORT")), 8080)
|
||||
```
|
||||
|
||||
### Logging Errors
|
||||
|
||||
```lux
|
||||
fn processWithLogging(input: String): Result<Output, String> with {Console} = {
|
||||
let result = process(input)
|
||||
match result {
|
||||
Ok(_) => result,
|
||||
Err(e) => {
|
||||
Console.print("Error processing input: " + e)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Error Types
|
||||
|
||||
Define specific error types:
|
||||
|
||||
```lux
|
||||
type ValidationError =
|
||||
| MissingField(String)
|
||||
| InvalidFormat(String, String) // field, expected format
|
||||
| OutOfRange(String, Int, Int) // field, min, max
|
||||
|
||||
fn validate(user: User): Result<User, ValidationError> = {
|
||||
if user.name == "" then Err(MissingField("name"))
|
||||
else if user.age < 0 then Err(OutOfRange("age", 0, 150))
|
||||
else if !isValidEmail(user.email) then Err(InvalidFormat("email", "user@domain.com"))
|
||||
else Ok(user)
|
||||
}
|
||||
|
||||
fn showError(e: ValidationError): String =
|
||||
match e {
|
||||
MissingField(f) => "Missing required field: " + f,
|
||||
InvalidFormat(f, fmt) => f + " must be in format: " + fmt,
|
||||
OutOfRange(f, min, max) =>
|
||||
f + " must be between " + toString(min) + " and " + toString(max)
|
||||
}
|
||||
```
|
||||
|
||||
## When to Use What
|
||||
|
||||
| Scenario | Use |
|
||||
|----------|-----|
|
||||
| Might not exist | `Option<T>` |
|
||||
| Can fail with reason | `Result<T, E>` |
|
||||
| Fatal error, stop execution | `Fail` effect |
|
||||
| Multiple error types | Custom error ADT |
|
||||
|
||||
## Summary
|
||||
|
||||
```lux
|
||||
// Option - maybe a value
|
||||
let opt: Option<Int> = Some(42)
|
||||
let none: Option<Int> = None
|
||||
|
||||
// Result - success or error
|
||||
let ok: Result<Int, String> = Ok(42)
|
||||
let err: Result<Int, String> = Err("failed")
|
||||
|
||||
// Fail effect - abort execution
|
||||
fn risky(): Int with {Fail} = Fail.fail("boom")
|
||||
|
||||
// Pattern match to handle
|
||||
match result {
|
||||
Ok(v) => handleSuccess(v),
|
||||
Err(e) => handleError(e)
|
||||
}
|
||||
```
|
||||
|
||||
## Next
|
||||
|
||||
[Chapter 9: Standard Library](09-stdlib.md) - Built-in functions and modules.
|
||||
Reference in New Issue
Block a user