Most programming languages treat errors as interruptions -- something went wrong, stop execution, report the problem. FLIN treats errors as information -- something went wrong, here is exactly what happened, here is the chain of events that led here, and here is what the calling code can do about it.
Session 137 introduced FLIN's error chaining and resilience patterns. The work was motivated by a simple observation: in a web application, errors are not exceptional. They are routine. A database query fails because the disk is full. An API call times out because the upstream service is slow. A user submits invalid data. A file upload exceeds the size limit. These are normal events in the life of a production application, and the language should make handling them natural, not painful.
The Problem with Traditional Error Handling
Most web frameworks offer two error handling mechanisms: exceptions (try/catch) and status codes (return values). Both have problems.
Exceptions are invisible. When a function throws an exception, the caller has no way to know which exceptions might be thrown without reading the implementation. The exception propagates up the call stack until someone catches it, potentially crossing module boundaries and producing confusing error messages far from the original failure.
Return codes are verbose. Checking every function's return value for errors produces code that is more error handling than business logic. Developers skip the checks, and unhandled errors become silent failures.
FLIN's approach combines the explicitness of return codes with the ergonomics of exceptions, plus a feature that neither typically provides: error chaining with context.
Error Values in FLIN
In FLIN, errors are values. They can be stored in variables, passed to functions, inspected, and enriched with context. The try keyword converts a potential error into a value:
// Traditional approach: crash on error
user = User.find(42) // Crashes if user doesn't exist// Resilient approach: handle the error result = try User.find(42)
if result is error { log_warn("User not found: " + result.message) return { error: "User not found" } }
user = result.value ```
The try keyword wraps the operation's result in a tagged union: either { value: ... } on success or { error: ... } on failure. The is error check is a pattern match against the error variant.
This is explicit -- the developer sees exactly where errors can occur -- but not verbose. The common case (no error) is one line of wrapping. The error case is handled close to where it occurs.
Error Chaining
The real power of FLIN's error system is chaining. When an error passes through multiple layers of code, each layer can add context without losing the original error information:
fn load_user_profile(user_id) {
user = try User.find(user_id)
.context("Loading user profile")if user is error { return user // Error now includes context }
posts = try Post.where(author_id == user_id).all .context("Loading user posts")
if posts is error { return posts }
avatar = try fetch_avatar(user.value.avatar_url) .context("Fetching avatar from CDN")
if avatar is error { // Avatar failure is non-critical -- continue without it log_warn(avatar.chain_message) avatar_url = "/default-avatar.png" } else { avatar_url = avatar.value }
{ user: user.value, posts: posts.value, avatar: avatar_url } } ```
The .context() method adds a layer to the error chain. When the error is eventually logged or displayed, the chain shows the full path:
Error: Fetching avatar from CDN
Caused by: HTTP request failed
Caused by: Connection timeout after 5000ms
Target: https://cdn.example.com/avatars/42.jpgThis chain is invaluable for debugging. Instead of a bare "Connection timeout" message, the developer sees exactly which operation triggered the timeout, which function initiated the HTTP request, and what the original intent was.
The Error Chain in Rust
Behind the scenes, FLIN's error chaining is implemented in the runtime as a linked list of error contexts:
#[derive(Debug, Clone)]
pub struct FlinError {
kind: ErrorKind,
message: String,
span: Option<Span>,
context: Vec<ErrorContext>,
source: Option<Box<FlinError>>,
}#[derive(Debug, Clone)] pub struct ErrorContext { message: String, span: Option, timestamp: Instant, }
impl FlinError { pub fn new(kind: ErrorKind, message: &str, span: Span) -> Self { Self { kind, message: message.to_string(), span: Some(span), context: Vec::new(), source: None, } }
pub fn with_context(mut self, message: &str, span: Option) -> Self { self.context.push(ErrorContext { message: message.to_string(), span, timestamp: Instant::now(), }); self }
pub fn chain_message(&self) -> String { let mut parts = vec![self.message.clone()]; for ctx in self.context.iter().rev() { parts.push(format!(" Caused by: {}", ctx.message)); } if let Some(ref source) = self.source { parts.push(format!(" Root cause: {}", source.message)); } parts.join("\n") } } ```
Each with_context() call pushes a new frame onto the context vector. The chain_message() method formats the full chain for logging or display. The source field links to the underlying error when a FLIN error wraps a Rust error (IO error, network error, parsing error).
Resilience Patterns
Error handling is about more than catching and reporting errors. It is about deciding what to do when errors occur. FLIN provides several resilience patterns that make this decision explicit.
Fallback Values
The try ... or pattern provides a default value when an operation fails:
// If cache lookup fails, query the database
user_count = try cache_get("user_count") or User.count// If config file is missing, use defaults config = try load_config("app.config") or { port: 3000, host: "localhost", debug: true }
// If image resize fails, use original thumbnail = try image_resize(photo, 200, 200) or photo ```
The or branch executes only if the try operation produces an error. This is syntactic sugar for the if result is error pattern, but significantly more readable for the common case of "try this, fall back to that."
Retry with Backoff
For transient failures (network timeouts, rate limits), FLIN provides a retry block:
// Retry up to 3 times with exponential backoff
response = retry(3, backoff: "exponential") {
fetch("https://api.example.com/data")
}if response is error { // All 3 attempts failed log_error("API permanently unavailable: " + response.chain_message) return { error: "Service unavailable" } } ```
The retry block executes the body up to N times. On each failure, it waits before retrying: 1 second, then 4 seconds, then 16 seconds (exponential backoff). If all attempts fail, the error from the last attempt is returned with context showing all attempts.
Circuit Breaker
For dependencies that may be down for extended periods, the circuit breaker pattern prevents wasting resources on requests that will certainly fail:
// Circuit breaker: stop trying after 5 consecutive failures
breaker = circuit_breaker("payment-api", threshold: 5, reset: 60)fn charge_customer(customer_id, amount) { if breaker.is_open { return error("Payment service temporarily unavailable") }
result = try stripe_checkout(amount, success_url, cancel_url)
if result is error { breaker.record_failure() return result.context("Charging customer " + customer_id) }
breaker.record_success() result.value } ```
After 5 consecutive failures, the circuit breaker opens and immediately returns an error without making the network call. After 60 seconds, the breaker enters a half-open state and allows one request through. If it succeeds, the breaker closes and normal operation resumes. If it fails, the breaker opens again.
Graceful Degradation
The most sophisticated resilience pattern is graceful degradation: the application continues to function with reduced capability when a subsystem fails.
route GET "/dashboard" {
guard auth// Critical: user data (fail the request if unavailable) user = User.find(session.user_id)
// Non-critical: recent activity (degrade to empty list) activity = try Activity .where(user_id == user.id) .order(created_at, "desc") .limit(10) .all or []
// Non-critical: recommendations (degrade to empty) recommendations = try fetch_recommendations(user.id) or []
// Non-critical: notification count (degrade to zero) notification_count = try cache_get("notif_count_" + user.id) or 0
In this pattern, only the user data is critical -- if it fails, the route returns a 500. Everything else is wrapped in try ... or with sensible defaults. If the recommendation engine is down, the dashboard still renders -- it just shows no recommendations. The user sees a functional page, not an error screen.
Error Categorization
FLIN categorizes errors by kind, enabling different handling strategies for different categories:
#[derive(Debug, Clone, PartialEq)]
pub enum ErrorKind {
// User errors (4xx) -- the request is wrong
ValidationError,
NotFound,
Unauthorized,
Forbidden,
Conflict,// System errors (5xx) -- something broke DatabaseError, NetworkError, TimeoutError, IOError,
// Logic errors -- bugs in the FLIN program TypeError, DivisionByZero, IndexOutOfBounds, StackOverflow,
// Resource errors -- limits reached MemoryLimitExceeded, RateLimitExceeded, FileSizeLimitExceeded, } ```
The HTTP server uses error categorization to select the appropriate status code automatically. A ValidationError returns 400. A NotFound returns 404. A DatabaseError returns 500. The developer does not need to manually map errors to HTTP status codes -- the runtime does it based on the error kind.
// This automatically returns HTTP 404
route GET "/api/users/:id" {
user = User.find(params.id) // Throws NotFound if missing
user
}// This automatically returns HTTP 400 route POST "/api/users" { validate { email: text @required @email name: text @required @minLength(2) } // Validation failure throws ValidationError save User { email: body.email, name: body.name } } ```
Error Reporting in Production
In production, error chains are logged with structured data that enables filtering, aggregation, and alerting:
fn log_error_chain(error: &FlinError, request: &Request) {
let log_entry = json!({
"level": "error",
"error_kind": format!("{:?}", error.kind),
"message": error.message,
"chain": error.chain_message(),
"request": {
"method": request.method,
"path": request.path,
"user_id": request.session.map(|s| s.user_id),
},
"source_location": error.span.map(|s| {
json!({
"file": s.file,
"line": s.line,
"column": s.column,
})
}),
"timestamp": chrono::Utc::now().to_rfc3339(),
});log::error!("{}", serde_json::to_string(&log_entry).unwrap()); } ```
The structured log entry includes the error kind (for filtering), the full chain (for debugging), the request context (for reproduction), and the source location (for code navigation). A monitoring system can alert on error_kind: "DatabaseError" without parsing free-text log messages.
The Philosophy
Error handling in FLIN is guided by one principle: errors are not exceptional; they are expected. A production application will encounter network timeouts, invalid input, missing records, and resource limits. The language should make handling these events as natural as handling the success case.
The combination of try for explicit error capture, .context() for error enrichment, or for fallback values, retry for transient failures, and circuit breakers for persistent failures gives FLIN developers a comprehensive toolkit for building resilient applications. None of these patterns are novel -- they exist in production systems at every major technology company. What is novel is having them built into the language runtime rather than bolted on as libraries.
Session 137 established FLIN's error philosophy. Every session after it built on the foundation: the production hardening phases (181-183) relied on error chaining for crash recovery, the integration tests (185) verified error propagation across subsystem boundaries, and the search caching system (187) uses fallback patterns for cache misses.
Errors are information. Treat them that way.
---
This is Part 186 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.
Series Navigation: - [185] Integration Tests Complete - [186] Error Resilience Patterns (you are here) - [187] Search Result Caching