Error Handling
Runtime failures in J2 are typed. Every error belongs to a class in one hierarchy, and handlers catch by class, so a program states exactly which failures it expects and lets the rest surface loudly.
try and else
A try block is followed by one or more else handlers. A
handler naming an error class catches errors of that class; a bare else
catches whatever the named handlers did not. Handlers are tried in the order
written.
try {
x = 10 / 0
} else ZeroDivisionError {
print("caught: division by zero")
} else {
print("caught: something else")
}
A parent class catches its descendants, so the specificity of the handler is up to
you. ArithmeticError catches both ZeroDivisionError and
OverflowError, and Error catches essentially everything:
try {
v = num("not a number") + 1
} else ConversionError {
print("bad input")
} else ArithmeticError {
print("bad math")
}
try {
y = 1 / 0
} else Error {
print("caught via the root class")
}
The hierarchy
Error
|- ArithmeticError
| |- ZeroDivisionError
| \- OverflowError
|- TypeError
|- ValueError
| \- ConversionError
|- LookupError
| |- IndexError
| \- KeyError
|- NameError
| \- MutabilityError
|- FlowError
| \- InfiniteFlowError
|- RuntimeError
| \- RecursionError
\- SyntaxError
Where the common errors come from:
| Error | Raised by |
|---|---|
ZeroDivisionError | dividing or taking a remainder by zero |
OverflowError | integer arithmetic exceeding 64 bits |
TypeError | an operation on the wrong kind of value, such as ordering two seqs |
ConversionError | num on unparseable text |
ValueError | invalid arguments, such as an fmt placeholder mismatch |
IndexError | a seq or text index out of range, or negative |
KeyError | reading a map key that is not present |
NameError | using a name that is not bound |
MutabilityError | reassigning a constant binding |
InfiniteFlowError | consuming an unbounded flow without a stopping condition |
RuntimeError | failed asserts, denied capabilities, I/O failures |
Recovering in place
Because handlers attach to a small try block rather than a whole
routine, the recover-and-continue pattern stays local. Summing the numeric column of
messy input, skipping bad rows:
rows = ["12", "7", "oops", "23"]
total := 0
bad := 0
for r in rows {
try {
total += num(r)
} else ConversionError {
bad += 1
}
}
print(fmt("total={} bad_rows={}", total, bad)) # total=42 bad_rows=1
Asserts
assert cond, "message" raises RuntimeError when the
condition is falsy, and assert_eq(a, b) does the same when its arguments
differ. Use them for invariants that should end the program, not for expected failures;
expected failures deserve a try.
balance = -5
assert balance >= 0, "balance must not be negative"
# RuntimeError: balance must not be negative
give passes through
Returning from a function is not a failure, so give is invisible to
error handling: it exits the function even from inside a try, and no
handler, not even a bare else, intercepts it.
func attempt(x) = {
try {
give x * 2
} else {
give -1
}
}
print(attempt(10)) # 20, the handler never runs
Uncaught errors
An error no handler catches ends the program with a nonzero exit status and a one-line report naming the class, the message, and the line:
ZeroDivisionError: division by zero (at line 3)
In this release errors originate from operations and asserts; there is no surface
syntax for raising a specific error class directly. When a function needs to signal
failure on its own terms, an assert with a clear message is the idiomatic
tool.