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:

ErrorRaised by
ZeroDivisionErrordividing or taking a remainder by zero
OverflowErrorinteger arithmetic exceeding 64 bits
TypeErroran operation on the wrong kind of value, such as ordering two seqs
ConversionErrornum on unparseable text
ValueErrorinvalid arguments, such as an fmt placeholder mismatch
IndexErrora seq or text index out of range, or negative
KeyErrorreading a map key that is not present
NameErrorusing a name that is not bound
MutabilityErrorreassigning a constant binding
InfiniteFlowErrorconsuming an unbounded flow without a stopping condition
RuntimeErrorfailed 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.