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 or the file is missing, those are environmental failures. The program may need to 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 that makes 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, and if an exception occurs the rescue clause runs in its place. Even so, 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,” and anything outside it is an invalid call that 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 dependable. If you use exceptions for contract failures, callers can become sloppy because the interface no longer states clear obligations.
When your own code raises, exception is exactly the value you raised which could be a string, a number, or any object. When the language itself raises in way of a failed contract, a division by zero, or an index out of range, exception is an object of a class that names the kind of failure:
nex> do
print(10 / 0)
rescue
print(exception)
end
Division by zero
Printing the exception, or joining it to a string with +, gives its message. To act on the kind of failure, test for its class with convert:
nex> do
print(10 / 0)
rescue
if convert exception to e: Division_by_Zero then
print("cannot divide: " + e.message)
end
end
"cannot divide: Division by zero"
Contract violations arrive the same way. A failed require is a Precondition_Violation, a failed ensure a Postcondition_Violation, a broken invariant an Invariant_Violation, and a failed assert an Assertion_Violation. Each carries the failed assertion’s label:
nex> function half(n: Integer): Integer
require
even: n % 2 = 0
do
result := n / 2
end
nex> do
print(half(7))
rescue
if convert exception to v: Precondition_Violation then
print("refused: " + v.message)
end
end
"refused: Precondition violation: even"
Being able to catch a contract violation does not change the advice of the previous section. A violation is a bug in the caller, not a condition to recover from; catch one only at a boundary where the program must keep running — to report the failure and carry on with the next request, say — never to paper over the call that broke the contract.
When several kinds of failure are possible, match on the exception. Since exception can hold any value, give the match an else, and re-raise what you do not handle:
nex> let scores := [90, 85]
nex> do
print(scores.get(5))
rescue
match exception of
Division_by_Zero then print("nothing to divide by")
Index_Out_Of_Bounds as e then print("no such score: " + e.message)
else
raise exception
end
end
"no such score: Index 5 out of bounds for length 2"
All of these classes inherit Exception, whose message is the text the failure is reported with. The others are Arithmetic_Overflow, Conversion_Error (for example "x".to_integer), Void_Access, No_Matching_Clause, Channel_Closed, and Host_Exception for anything the platform raises that has no more specific class. The reference lists them all.
Your own failures can be classes too. Inherit Exception, and the caller can tell them apart in exactly the same way:
nex> class Config_Error
inherit Exception
create
make(m: String) do
Exception.make(m)
end
end
nex> do
raise create Config_Error.make("missing setting: port")
rescue
if convert exception to c: Config_Error then
print("config problem: " + c.message)
else
raise exception
end
end
"config problem: missing setting: port"
Raised and not caught, such an exception is reported by its message, just like a built-in one:
nex> raise create Config_Error.make("missing setting: port")
Error: missing setting: port
A rescue block should handle the failure in a way that makes sense for the surrounding routine, not merely note that something went wrong.
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, such as a missing file. 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
Parse_Ok as ok then
print("got: " + ok.value.to_string)
Parse_Error as err then
print("failed: " + err.reason)
end
Compared to raising an exception, this approach puts the failure in the routine’s signature: a caller cannot receive a Parse_Result without being made to decide what an error means. An exception, even a typed one, is noticed only when it is raised, and a missing rescue shows up at run time. A forgotten match branch shows up at 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.
Result and OptionA success-or-failure type is common enough that you rarely need to write your own. The standard library ships a generic Result — load it with intern:
intern data/Result
function parse_positive(text: String): Result[Integer, String]
do
let n := text.to_integer
if n > 0 then
result := create Ok[Integer, String].make(n)
else
result := create Err[Integer, String].make("must be positive")
end
end
A Result[T, E] holds either an Ok value of type T or an Err value of type E. The two type parameters are independent, so an error can travel up through several layers without being rewrapped. Callers match on it as usual, destructuring the payload directly:
match parse_positive("42") of
Ok(value) then print(value)
Err(error) then print(error)
end
Querying and unwrapping are methods: r.is_ok(), r.is_err(), and r.unwrap_or(0), which returns the value or a fallback when the result is an Err. Transforming a result, though, uses free functions instead, since each one introduces a new type parameter. result_map applies a function to the success value. result_and_then chains a further fallible step, short-circuiting on the first error. result_map_err converts the error into a caller’s own error type.
let doubled: Result[Integer, String] :=
result_map(parse_positive("21"), fn (x: Integer): Integer do result := x * 2 end)
Option is Result’s companion, for a value that may simply be absent — a lookup that misses, a first element that doesn’t exist. intern data/Option brings in Option[T] along with its Some and None variants, the is_some/is_none/get_or methods, and the option_map/option_and_then/option_filter combinators. Unlike a detachable ?T, which might be nil, an Option is a sealed type: a match over it is checked for exhaustiveness, and it survives cleanly through generic code.
intern data/Option
let found: Option[Integer] := create Some[Integer].make(10)
match found of
Some(value) then print(value)
None then print("absent")
end
Use the hand-written sealed type from the previous section when the variants need their own methods or contracts. Use the standard Result and Option when plain success/failure or presence/absence is all you need. Both are documented in Appendix C.
raise signals an exception; rescue handles it; retry tries the protected block againrescue as an object of a built-in exception class; test for one with convert or match, and inherit Exception for failures of your ownmatch 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.