Error Handling

Keikaku provides structured error handling through attempt/recover blocks and the ability to inject exceptions into running generators.

Error Handling Primitives

  • attempt - Execute code that might fail
  • recover as err - Catch and handle errors
  • disrupt(gen, error) - Throw exception into generator

Attempt & Recover (Try/Catch)

Wrap risky code in attempt blocks and handle errors in recover blocks.

KEIKAKU Protocol
1# Basic error handling
2attempt:
3 declare("Attempting risky operation...")
4 result := 10 / 0 # Division by zero!
5 declare("This won't print")
6recover as error:
7 declare("Caught error:", error)
8
9declare("Program continues normally")
10
11# Output:
12# Attempting risky operation...
13# Caught error: Division by zero
14# Program continues normally

Error Recovery Patterns

KEIKAKU Protocol
1# Return default value on error
2protocol safe_divide(a, b):
3 attempt:
4 foresee b == 0:
5 yield 0 # Safe default
6 otherwise:
7 yield a / b
8 recover as e:
9 declare("Division error:", e)
10 yield 0
11
12declare(safe_divide(10, 2)) # 5
13declare(safe_divide(10, 0)) # 0 (safe default)
14
15# Retry pattern
16protocol fetch_with_retry(url, max_retries):
17 attempts := 0
18 cycle while attempts < max_retries:
19 attempts = attempts + 1
20 attempt:
21 declare("Attempt", attempts, "to fetch", url)
22 # Simulate random failure
23 foresee random(1, 10) > 5:
24 yield "Success!"
25 otherwise:
26 x := unknown_var # Force error
27 recover as e:
28 declare("Failed, retrying...")
29 sleep(100)
30
31 yield "All retries exhausted"

Nested Error Handling

KEIKAKU Protocol
1# Inner errors can propagate to outer handlers
2attempt:
3 declare("Outer attempt")
4
5 attempt:
6 declare("Inner attempt")
7 x := undefined_variable
8 declare("Won't reach here")
9 recover as inner_err:
10 declare("Inner caught:", inner_err)
11 # Re-raise by causing another error
12 foresee text(inner_err) == "important":
13 y := another_undefined # Propagate
14
15 declare("Outer continues")
16
17recover as outer_err:
18 declare("Outer caught:", outer_err)

Generator Exception Injection

The disrupt(generator, error) function throws an exception INTO a running generator, which can be caught with attempt/recover inside the generator.

KEIKAKU Protocol
1# Generator that handles injected exceptions
2sequence worker():
3 task_count := 0
4 cycle while true:
5 attempt:
6 task_count = task_count + 1
7 yield "Completed task " + text(task_count)
8 recover as err:
9 declare("Worker interrupted:", err)
10 yield "Recovered from " + text(err)
11
12gen := worker()
13declare(proceed(gen)) # Completed task 1
14declare(proceed(gen)) # Completed task 2
15declare(disrupt(gen, "Timeout")) # Worker interrupted: Timeout
16 # Recovered from Timeout
17declare(proceed(gen)) # Completed task 3

Graceful Shutdown Pattern

KEIKAKU Protocol
1# Generator with cleanup on exception
2sequence file_processor(files):
3 declare("Opening resources...")
4
5 cycle through files as file:
6 attempt:
7 declare("Processing:", file)
8 yield "Processed " + file
9 recover as err:
10 declare("Error processing", file, "-", err)
11 # Cleanup before dying
12 declare("Closing resources...")
13 yield "Shutdown complete"
14
15gen := file_processor(["a.txt", "b.txt", "c.txt"])
16
17declare(proceed(gen)) # Processing: a.txt
18declare(proceed(gen)) # Processing: b.txt
19
20# Inject shutdown signal
21declare(disrupt(gen, "SIGTERM"))
22# Output:
23# Error processing b.txt - SIGTERM
24# Closing resources...
25# Shutdown complete

Common Error Patterns

Validation with Early Return

KEIKAKU Protocol
1protocol validate_user(user):
2 errors := []
3
4 foresee measure(user["name"]) < 2:
5 push(errors, "Name too short")
6
7 foresee user["age"] < 0:
8 push(errors, "Age cannot be negative")
9
10 foresee measure(errors) > 0:
11 yield {"valid": false, "errors": errors}
12 otherwise:
13 yield {"valid": true, "data": user}
14
15result := validate_user({"name": "A", "age": -5})
16foresee result["valid"] == false:
17 declare("Validation failed:", result["errors"])
18# Output: Validation failed: ["Name too short", "Age cannot be negative"]

Error Accumulator

KEIKAKU Protocol
1# Collect all errors instead of failing on first
2sequence process_batch(items):
3 errors := []
4 successes := 0
5
6 cycle through items as item:
7 attempt:
8 foresee item < 0:
9 x := invalid # Force error for negative
10 successes = successes + 1
11 yield "Processed: " + text(item)
12 recover as e:
13 push(errors, {"item": item, "error": text(e)})
14 yield "Skipped: " + text(item)
15
16 yield {
17 "total": measure(items),
18 "successes": successes,
19 "errors": errors
20 }
21
22items := [1, 2, -3, 4, -5, 6]
23cycle through process_batch(items) as result:
24 declare(result)
25
26# Final result shows: 4 successes, 2 errors

Circuit Breaker Pattern

KEIKAKU Protocol
1# Stop calling a failing service
2sequence circuit_breaker(threshold):
3 failures := 0
4 is_open := false
5
6 cycle while true:
7 foresee is_open:
8 yield {"status": "circuit_open", "message": "Service unavailable"}
9 otherwise:
10 received := receive()
11 attempt:
12 # Simulate calling service
13 foresee random(1, 10) > 7:
14 failures = 0 # Reset on success
15 yield {"status": "success", "data": received}
16 otherwise:
17 x := fail # Simulate failure
18 recover as e:
19 failures = failures + 1
20 foresee failures >= threshold:
21 is_open = true
22 declare("Circuit opened after", threshold, "failures")
23 yield {"status": "error", "failures": failures}
24
25breaker := circuit_breaker(3)
26cycle from 0 to 10 as i:
27 declare(transmit(breaker, "request " + text(i)))

Best Practices

  • 1. Be specific in recover blocks - Handle known error types appropriately
  • 2. Always clean up resources - Files, connections should be closed on error
  • 3. Log errors before recovering - Don't silently swallow errors
  • 4. Use disrupt() for generator control - Clean way to signal shutdown
  • 5. Prefer validation over exception - Check conditions before they fail