Contracts are for broken obligations between parts of a program. Exceptions are for failures that can happen even when everyone uses the interface correctly.
If a caller violates amount <= balance, that is a contract problem. If the network is down, the file is missing, or the external service is temporarily unavailable, those are environmental failures. The program may need to report them, recover from them, or retry.
Nex provides three related constructs:
raise to signal an exceptionrescue to handle itretry to run the protected block againThe simplest form is raise <expression>:
nex> raise "network unavailable"
Error: network unavailable
The raised value becomes the current exception. In a rescue block, it is available through the special name exception.
Raising an exception stops the current protected block immediately. Control passes to the nearest enclosing rescue.
The general form is:
do
print("trying")
rescue
print("recovering from " + exception.to_string)
end
Example:
nex> do
print("before")
raise "something went wrong"
print("after")
rescue
print("rescued: " + exception.to_string)
end
"before"
"rescued: something went wrong"
The line print("after") never runs because raise aborts the protected block.
Some failures are temporary. In such cases, a rescue block may try again with retry.
nex> let attempts := 0
nex> do
attempts := attempts + 1
if attempts < 3 then
raise "not ready yet"
end
print("done on attempt " + attempts.to_string)
rescue
print("failed: " + exception.to_string)
retry
end
"failed: not ready yet"
"failed: not ready yet"
"done on attempt 3"
retry jumps back to the start of the do block and runs it again.
This is powerful, but it must be used carefully. A retry with no progress toward success becomes an infinite loop in disguise.
Constructors, methods, and functions may also have rescue clauses:
nex> function read_config(path: String): String
do
raise "file not found"
rescue
result := "default-config"
end
The routine body is attempted. If an exception occurs, the rescue clause runs.
The routine should still return to a meaningful state. A rescue clause that simply swallows every error without restoring a sensible result is usually a design mistake.
Suppose we have:
withdraw(amount: Real)
require
positive_amount: amount > 0.0
enough: amount <= balance
do
balance := balance - amount
end
Should withdraw raise an exception when the balance is too small instead of using a precondition?
Usually, no.
Insufficient balance in this design is a caller error. The routine’s legal input space is “positive amounts no larger than the balance.” Anything else is an invalid call and should fail as a contract violation.
Use exceptions when the failure is not a misuse of the routine but a condition arising during normal correct use:
This distinction keeps designs honest. If you use exceptions for contract failures, callers can become sloppy because the interface no longer states clear obligations.
A rescue block should handle the failure in a way that makes sense for the surrounding routine.
Poor rescue:
rescue
print("error")
end
This loses information and often leaves the computation in an unknown state.
Better rescue:
rescue
print("could not load settings: " + exception.to_string)
result := default_settings()
end
Or, if the routine cannot continue meaningfully:
rescue
print("fatal: " + exception.to_string)
raise exception
end
Recovery should either:
Anything else tends to hide bugs.
Unbounded retry is dangerous. Give it a stopping rule.
nex> function connect_with_retry(): String
do
let attempts := 0
do
attempts := attempts + 1
if attempts < 3 then
raise "temporary connection error"
end
result := "connected"
rescue
if attempts < 3 then
retry
else
raise exception
end
end
end
This routine retries twice, then gives up. The rescue logic is controlled and explicit.
Here is a small example that separates routine obligations from environmental uncertainty:
nex> function parse_positive(text: String): Integer
require
not_empty: text.length > 0
do
let value := text.to_integer
if value <= 0 then
raise "number must be positive"
end
result := value
rescue
raise "invalid positive integer: " + text
end
The routine uses a precondition for one issue and an exception for another:
That balance is not arbitrary. It reflects the routine’s role in the design. If the caller is expected to pass non-empty strings, make it a contract. If the content may legitimately fail to parse, raise or handle an exception.
raise and rescue are the right tool for truly exceptional conditions — file not found, network down, service unavailable. But sometimes a routine naturally has two outcomes, both expected: a parse may succeed or it may fail; a lookup may find the key or it may not. Expressing these as exceptions forces the caller to use a rescue block for what is really an ordinary control flow decision.
Sealed classes and the match statement provide an alternative that keeps the failure path explicit in the type signature. Define a sealed result type with one variant for each outcome:
sealed deferred class Parse_Result
end
class Parse_Ok
inherit Parse_Result
feature value: Integer
create make(v: Integer) do value := v end
end
class Parse_Error
inherit Parse_Result
feature reason: String
create make(r: String) do reason := r end
end
A routine returns the sealed type instead of raising:
function parse_positive(text: String): Parse_Result
do
let n := text.to_integer
if n > 0 then
result := create Parse_Ok.make(n)
else
result := create Parse_Error.make("must be positive")
end
end
The caller handles both cases with match. The typechecker verifies that neither branch is forgotten — forgetting the error case is a compile-time error, not a silent runtime failure:
let outcome: Parse_Result := parse_positive("42")
match outcome of
when Parse_Ok as ok then
print("got: " + ok.value.to_string)
when Parse_Error as err then
print("failed: " + err.reason)
end
Compared to string-based exceptions, this approach gives the caller a typed handle on the error — err.reason is a String field, not an opaque exception value — and moves the handling obligation from runtime to compile time.
Use raise/rescue for conditions that are unexpected during correct operation. Use sealed result types when both success and failure are normal outcomes that every caller must actively decide how to handle.
raise signals an exception; rescue handles it; retry tries the protected block againmatch when both success and failure are ordinary outcomes — the typechecker enforces that every variant is handled1. Write a do ... rescue ... end block that raises "too small" until a counter reaches 5, then prints "ok". Use retry.
2. Define a function safe_reciprocal(x: Real): Real that raises an exception when x = 0.0. Then wrap a call in a rescue block that prints a fallback message.
3. Rewrite a routine of your own choosing so that it distinguishes clearly between a contract violation and an exception-producing environmental failure.
4. Improve the connect_with_retry routine so that it prints the attempt number each time it retries.
5.* Design a small class File_Cache whose load(path: String): String routine first tries to read from memory, then from disk, and uses rescue logic to recover from a missing file by returning a built-in default. State what should be a precondition, what should be an exception, and why.