feat: add wasm build targets, profile hooks, os_demo example, and restructure manual source
- Add `wasm`, `wasm-clean`, and `install-emscripten` phony targets to Makefile for WebAssembly cross-compilation
- Insert `WREN_PROFILE_ENTER`/`WREN_PROFILE_EXIT` macros in `wren_vm.h` and `wren_vm.c` to support optional runtime profiling via `WREN_PROFILE_ENABLED`
- Create `example/os_demo.wren` demonstrating Platform, Process, and conditional exit usage
- Update `example/regex_demo.wren` to import `Match` and exercise Match object properties, groups, and `matchAll`
- Remove static HTML manual pages (`base64.html`, `dns.html`, `json.html`) and replace with structured `manual_src/` directory containing Jinja2 templates, YAML metadata, and content pages
- Expand `README.md` with build targets table, manual building instructions, source layout, and guide for adding new module documentation
2026-01-26 05:12:14 +01:00
|
|
|
{# retoor <retoor@molodetz.nl> #}
|
|
|
|
|
{% extends 'page.html' %}
|
|
|
|
|
|
|
|
|
|
{% set page_title = "Error Handling" %}
|
|
|
|
|
{% set breadcrumb = [{"url": "howto/index.html", "title": "How-To Guides"}, {"title": "Error Handling"}] %}
|
|
|
|
|
{% set prev_page = {"url": "howto/async-operations.html", "title": "Async Operations"} %}
|
|
|
|
|
{% set next_page = {"url": "contributing/index.html", "title": "Contributing"} %}
|
|
|
|
|
|
|
|
|
|
{% block article %}
|
feat: add example scripts for argparse, bytes, dataset, html, markdown, uuid, wdantic, and web chat modules
Add comprehensive demo scripts showcasing the usage of multiple Wren modules including argument parsing, byte manipulation, in-memory dataset operations, HTML encoding/slugification, markdown-to-HTML conversion, UUID generation/validation, schema validation with wdantic, and a full web chat application with REST endpoints.
2026-01-24 23:40:22 +01:00
|
|
|
<h1>Error Handling</h1>
|
|
|
|
|
|
|
|
|
|
<p>Wren uses fibers for error handling. The <code>fiber.try()</code> method catches errors without crashing your program.</p>
|
|
|
|
|
|
|
|
|
|
<h2>Basic Try/Catch Pattern</h2>
|
|
|
|
|
<pre><code>var fiber = Fiber.new {
|
|
|
|
|
Fiber.abort("Something went wrong!")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = fiber.try()
|
|
|
|
|
|
|
|
|
|
if (fiber.error) {
|
|
|
|
|
System.print("Error: %(fiber.error)")
|
|
|
|
|
} else {
|
|
|
|
|
System.print("Result: %(result)")
|
|
|
|
|
}</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Throw an Error</h2>
|
|
|
|
|
<pre><code>Fiber.abort("This is an error message")</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Catch Specific Error Types</h2>
|
|
|
|
|
<pre><code>var fiber = Fiber.new {
|
|
|
|
|
var x = null
|
|
|
|
|
return x.count // Error: null has no method 'count'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = fiber.try()
|
|
|
|
|
|
|
|
|
|
if (fiber.error) {
|
|
|
|
|
if (fiber.error.contains("null")) {
|
|
|
|
|
System.print("Null reference error")
|
|
|
|
|
} else {
|
|
|
|
|
System.print("Other error: %(fiber.error)")
|
|
|
|
|
}
|
|
|
|
|
}</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Safe File Read</h2>
|
|
|
|
|
<pre><code>import "io" for File
|
|
|
|
|
|
|
|
|
|
var safeRead = Fn.new { |path|
|
|
|
|
|
var fiber = Fiber.new { File.read(path) }
|
|
|
|
|
var content = fiber.try()
|
|
|
|
|
|
|
|
|
|
if (fiber.error) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return content
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var content = safeRead.call("config.txt")
|
|
|
|
|
if (content) {
|
|
|
|
|
System.print(content)
|
|
|
|
|
} else {
|
|
|
|
|
System.print("Could not read file")
|
|
|
|
|
}</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Safe JSON Parse</h2>
|
|
|
|
|
<pre><code>import "json" for Json
|
|
|
|
|
|
|
|
|
|
var safeParse = Fn.new { |jsonStr|
|
|
|
|
|
var fiber = Fiber.new { Json.parse(jsonStr) }
|
|
|
|
|
var data = fiber.try()
|
|
|
|
|
|
|
|
|
|
if (fiber.error) {
|
|
|
|
|
System.print("Invalid JSON: %(fiber.error)")
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var data = safeParse.call('{"valid": true}') // Works
|
|
|
|
|
var bad = safeParse.call('not json') // Returns null</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Safe HTTP Request</h2>
|
|
|
|
|
<pre><code>import "http" for Http
|
|
|
|
|
|
|
|
|
|
var safeFetch = Fn.new { |url|
|
|
|
|
|
var fiber = Fiber.new { Http.get(url) }
|
|
|
|
|
var response = fiber.try()
|
|
|
|
|
|
|
|
|
|
if (fiber.error) {
|
|
|
|
|
return {"error": fiber.error, "ok": false}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (response.statusCode >= 400) {
|
|
|
|
|
return {"error": "HTTP %(response.statusCode)", "ok": false}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {"data": response.json, "ok": true}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = safeFetch.call("https://api.example.com/data")
|
|
|
|
|
if (result["ok"]) {
|
|
|
|
|
System.print(result["data"])
|
|
|
|
|
} else {
|
|
|
|
|
System.print("Error: %(result["error"])")
|
|
|
|
|
}</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Input Validation</h2>
|
|
|
|
|
<pre><code>var validateEmail = Fn.new { |email|
|
|
|
|
|
if (email == null) {
|
|
|
|
|
Fiber.abort("Email is required")
|
|
|
|
|
}
|
|
|
|
|
if (!email.contains("@")) {
|
|
|
|
|
Fiber.abort("Invalid email format")
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var validate = Fn.new { |email|
|
|
|
|
|
var fiber = Fiber.new { validateEmail.call(email) }
|
|
|
|
|
fiber.try()
|
|
|
|
|
return fiber.error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
System.print(validate.call(null)) // Email is required
|
|
|
|
|
System.print(validate.call("invalid")) // Invalid email format
|
|
|
|
|
System.print(validate.call("a@b.com")) // null (no error)</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Result Type Pattern</h2>
|
|
|
|
|
<pre><code>class Result {
|
|
|
|
|
construct ok(value) {
|
|
|
|
|
_value = value
|
|
|
|
|
_error = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
construct error(message) {
|
|
|
|
|
_value = null
|
|
|
|
|
_error = message
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isOk { _error == null }
|
|
|
|
|
isError { _error != null }
|
|
|
|
|
value { _value }
|
|
|
|
|
error { _error }
|
|
|
|
|
|
|
|
|
|
unwrap {
|
|
|
|
|
if (isError) Fiber.abort(_error)
|
|
|
|
|
return _value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unwrapOr(default) { isOk ? _value : default }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var divide = Fn.new { |a, b|
|
|
|
|
|
if (b == 0) {
|
|
|
|
|
return Result.error("Division by zero")
|
|
|
|
|
}
|
|
|
|
|
return Result.ok(a / b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = divide.call(10, 2)
|
|
|
|
|
if (result.isOk) {
|
|
|
|
|
System.print("Result: %(result.value)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var bad = divide.call(10, 0)
|
|
|
|
|
if (bad.isError) {
|
|
|
|
|
System.print("Error: %(bad.error)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
System.print(divide.call(10, 5).unwrapOr(0)) // 2
|
|
|
|
|
System.print(divide.call(10, 0).unwrapOr(0)) // 0</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Assert Function</h2>
|
|
|
|
|
<pre><code>var assert = Fn.new { |condition, message|
|
|
|
|
|
if (!condition) {
|
|
|
|
|
Fiber.abort("Assertion failed: %(message)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var processUser = Fn.new { |user|
|
|
|
|
|
assert.call(user != null, "User is required")
|
|
|
|
|
assert.call(user["name"] != null, "User name is required")
|
|
|
|
|
assert.call(user["age"] >= 0, "Age must be non-negative")
|
|
|
|
|
|
|
|
|
|
System.print("Processing %(user["name"])...")
|
|
|
|
|
}</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Multiple Error Sources</h2>
|
|
|
|
|
<pre><code>import "http" for Http
|
|
|
|
|
import "json" for Json
|
|
|
|
|
|
|
|
|
|
var fetchAndParse = Fn.new { |url|
|
|
|
|
|
var httpFiber = Fiber.new { Http.get(url) }
|
|
|
|
|
var response = httpFiber.try()
|
|
|
|
|
|
|
|
|
|
if (httpFiber.error) {
|
|
|
|
|
return {"error": "Network error: %(httpFiber.error)"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (response.statusCode != 200) {
|
|
|
|
|
return {"error": "HTTP error: %(response.statusCode)"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var jsonFiber = Fiber.new { Json.parse(response.body) }
|
|
|
|
|
var data = jsonFiber.try()
|
|
|
|
|
|
|
|
|
|
if (jsonFiber.error) {
|
|
|
|
|
return {"error": "Parse error: %(jsonFiber.error)"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {"data": data}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = fetchAndParse.call("https://api.example.com/data")
|
|
|
|
|
if (result.containsKey("error")) {
|
|
|
|
|
System.print(result["error"])
|
|
|
|
|
} else {
|
|
|
|
|
System.print(result["data"])
|
|
|
|
|
}</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Error Logging</h2>
|
|
|
|
|
<pre><code>import "datetime" for DateTime
|
|
|
|
|
import "io" for File
|
|
|
|
|
|
|
|
|
|
class Logger {
|
|
|
|
|
construct new(logFile) {
|
|
|
|
|
_logFile = logFile
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(level, message) {
|
|
|
|
|
var timestamp = DateTime.now().toString
|
|
|
|
|
var entry = "[%(timestamp)] [%(level)] %(message)\n"
|
|
|
|
|
|
|
|
|
|
var existing = ""
|
|
|
|
|
var fiber = Fiber.new { File.read(_logFile) }
|
|
|
|
|
existing = fiber.try() || ""
|
|
|
|
|
|
|
|
|
|
File.write(_logFile, existing + entry)
|
|
|
|
|
|
|
|
|
|
if (level == "ERROR") {
|
|
|
|
|
System.print("ERROR: %(message)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
error(message) { log("ERROR", message) }
|
|
|
|
|
warn(message) { log("WARN", message) }
|
|
|
|
|
info(message) { log("INFO", message) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var logger = Logger.new("app.log")
|
|
|
|
|
|
|
|
|
|
var safeDivide = Fn.new { |a, b|
|
|
|
|
|
if (b == 0) {
|
|
|
|
|
logger.error("Division by zero: %(a) / %(b)")
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return a / b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = safeDivide.call(10, 0)</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Cleanup with Finally Pattern</h2>
|
|
|
|
|
<pre><code>import "sqlite" for Sqlite
|
|
|
|
|
|
|
|
|
|
var withDatabase = Fn.new { |dbPath, operation|
|
|
|
|
|
var db = Sqlite.open(dbPath)
|
|
|
|
|
var result = null
|
|
|
|
|
var error = null
|
|
|
|
|
|
|
|
|
|
var fiber = Fiber.new { operation.call(db) }
|
|
|
|
|
result = fiber.try()
|
|
|
|
|
error = fiber.error
|
|
|
|
|
|
|
|
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
Fiber.abort(error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var users = withDatabase.call("app.db", Fn.new { |db|
|
|
|
|
|
return db.execute("SELECT * FROM users")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
System.print(users)</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Custom Error Class</h2>
|
|
|
|
|
<pre><code>class AppError {
|
|
|
|
|
construct new(code, message) {
|
|
|
|
|
_code = code
|
|
|
|
|
_message = message
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
code { _code }
|
|
|
|
|
message { _message }
|
|
|
|
|
toString { "[%(code)] %(message)" }
|
|
|
|
|
|
|
|
|
|
static notFound(resource) {
|
|
|
|
|
return AppError.new("NOT_FOUND", "%(resource) not found")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static validation(field, reason) {
|
|
|
|
|
return AppError.new("VALIDATION", "%(field): %(reason)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static unauthorized() {
|
|
|
|
|
return AppError.new("UNAUTHORIZED", "Authentication required")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var findUser = Fn.new { |id|
|
|
|
|
|
if (id == null) {
|
|
|
|
|
Fiber.abort(AppError.validation("id", "is required").toString)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var user = null
|
|
|
|
|
|
|
|
|
|
if (user == null) {
|
|
|
|
|
Fiber.abort(AppError.notFound("User %(id)").toString)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return user
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fiber = Fiber.new { findUser.call(null) }
|
|
|
|
|
fiber.try()
|
|
|
|
|
System.print(fiber.error) // [VALIDATION] id: is required</code></pre>
|
|
|
|
|
|
|
|
|
|
<h2>Retry on Error</h2>
|
|
|
|
|
<pre><code>import "timer" for Timer
|
|
|
|
|
|
|
|
|
|
var retry = Fn.new { |operation, maxAttempts, delayMs|
|
|
|
|
|
var lastError = null
|
|
|
|
|
|
|
|
|
|
for (i in 1..maxAttempts) {
|
|
|
|
|
var fiber = Fiber.new { operation.call() }
|
|
|
|
|
var result = fiber.try()
|
|
|
|
|
|
|
|
|
|
if (!fiber.error) {
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastError = fiber.error
|
|
|
|
|
System.print("Attempt %(i) failed: %(lastError)")
|
|
|
|
|
|
|
|
|
|
if (i < maxAttempts) {
|
|
|
|
|
Timer.sleep(delayMs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Fiber.abort("All %(maxAttempts) attempts failed. Last error: %(lastError)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = retry.call(Fn.new {
|
|
|
|
|
return "success"
|
|
|
|
|
}, 3, 1000)</code></pre>
|
|
|
|
|
|
|
|
|
|
<div class="admonition tip">
|
|
|
|
|
<div class="admonition-title">Best Practices</div>
|
|
|
|
|
<ul>
|
|
|
|
|
<li>Always wrap external calls (HTTP, file I/O) in fibers</li>
|
|
|
|
|
<li>Provide meaningful error messages</li>
|
|
|
|
|
<li>Log errors for debugging</li>
|
|
|
|
|
<li>Fail fast on programming errors, recover from user errors</li>
|
|
|
|
|
<li>Clean up resources (close files, connections) even on error</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="admonition note">
|
|
|
|
|
<div class="admonition-title">See Also</div>
|
|
|
|
|
<p>For more on fibers, see the <a href="../language/fibers.html">Fibers language guide</a>.</p>
|
|
|
|
|
</div>
|
feat: add wasm build targets, profile hooks, os_demo example, and restructure manual source
- Add `wasm`, `wasm-clean`, and `install-emscripten` phony targets to Makefile for WebAssembly cross-compilation
- Insert `WREN_PROFILE_ENTER`/`WREN_PROFILE_EXIT` macros in `wren_vm.h` and `wren_vm.c` to support optional runtime profiling via `WREN_PROFILE_ENABLED`
- Create `example/os_demo.wren` demonstrating Platform, Process, and conditional exit usage
- Update `example/regex_demo.wren` to import `Match` and exercise Match object properties, groups, and `matchAll`
- Remove static HTML manual pages (`base64.html`, `dns.html`, `json.html`) and replace with structured `manual_src/` directory containing Jinja2 templates, YAML metadata, and content pages
- Expand `README.md` with build targets table, manual building instructions, source layout, and guide for adding new module documentation
2026-01-26 05:12:14 +01:00
|
|
|
{% endblock %}
|