Design notes on the current exception system and proposed improvements.
Standard exception handling. try(code) runs code in a child coroutine. If an exception is raised, it's captured on the coroutine and returned. catch filters by exception type. pass re-raises.
e := try(Exception raise("boom"))
e catch(Exception, writeln(e error))Added in the stackless branch. withHandler installs a handler on the coroutine's handler stack, evaluates the body, then removes the handler. signal walks the handler stack to find a matching handler and calls it as a subroutine — the signaler's frames remain on the stack.
result := withHandler(Exception, block(exc, resume,
"default value"
),
Exception signal("something went wrong")
)
// result is "default value"The handler runs as a normal block activation with its own frame. Whatever the handler returns becomes the return value of signal() at the call site. No coroutine switch, no frame manipulation, no callcc.
_Resumption is a placeholder object whose invoke(v) returns v. It exists for API consistency but is functionally the identity.
CL separates three concerns:
;; Low-level: offers restarts, doesn't choose
(defun parse-entry (line)
(restart-case (actually-parse line)
(skip-entry () nil)
(use-value (v) v)))
;; High-level: chooses policy, doesn't know recovery details
(handler-bind ((malformed-entry
(lambda (c) (invoke-restart 'skip-entry))))
(parse-log-file stream))Key features Io lacks:
handler-bind handler can return normally without invoking a restart, and the search continues to the next handler. In Io, the handler's return value is always used.Handler receives the exception object with a rich protocol:
ex resume: value — resume at signal site with value (non-local return from handler)ex return: value — return from the protected blockex retry — re-run the protected blockex pass — delegate to outer handlerex outer — invoke outer handler, then resumeMore expressive than Io's current system but lacks CL's restart separation.
The most impactful addition. Restarts let the signaler offer recovery strategies without choosing one, and the handler choose a strategy without knowing recovery details.
Possible Io API:
// Low-level code establishes restarts
parseEntry := method(line,
withRestarts(
list(
Restart clone setName("skipEntry") setAction(block(nil)),
Restart clone setName("useValue") setAction(block(v, v))
),
actuallyParse(line)
)
)
// High-level code picks a policy
withHandler(MalformedEntry, block(exc, resume,
invokeRestart("skipEntry")
),
parseLogFile(stream)
)Implementation: a restart registry (List) on the Coroutine, similar to handlerStack. withRestarts pushes entries, invokeRestart walks the registry. Pure Io-level code, no VM changes needed.
Allow a handler to decline (pass to next handler) instead of always producing a value. Could use a sentinel:
withHandler(Exception, block(exc, resume,
if(exc error containsSeq("fatal"),
decline // search continues to next handler
,
"recovered"
)
),
body
)Add methods to the exception object for retry, return(value), outer. These would use the eval loop's existing stop-status mechanism or handler stack walking.
When an unhandled signal reaches the top level with available restarts, present them to the user:
Error: Malformed entry at line 42
Available restarts:
0: [skipEntry] Skip this entry
1: [useValue] Supply a replacement value
2: [abort] Abort
Pick a restart:This requires restarts (direction 1) and REPL integration.
Start with restarts (direction 1). They provide the biggest leverage — decoupling error policy from recovery mechanism — and map naturally to Io's prototype model. A Restart is just an Object with name and action slots. The handler/restart registries are Lists on the Coroutine. No VM changes, no new primitives, just Io-level code building on withHandler.
Decline (direction 2) is a small addition on top. Interactive selection (direction 4) is the long-term payoff but requires restarts first.