Errors
Throwing
Only objects extending Error can be thrown.
throw RuntimeError("failed");
Throw errors when the caller can reasonably recover or report a domain-specific failure. Use return values for ordinary negative results such as "not found" when absence is expected.
Catching
try {
risky();
} catch (IOError e) {
io.println("io failed: " + e.message);
} catch (Error e) {
io.println("unexpected: " + e.message);
} finally {
cleanup();
}
finally runs after the try/catch path completes.
Catch clauses are checked in order. Put specific error classes before broader
base classes. catch (Error e) acts as a catch-all for any throwable error.
An untyped catch { ... } block also catches everything but does not bind the
error value.
Geblang converts recoverable host/runtime failures into language-level
exceptions. For example, file and network failures such as a missing file,
connection refusal, or trying to bind a port that is already in use are raised
as IOError and can be caught. Unrecoverable interpreter and bytecode integrity
failures still abort execution because they indicate an implementation or
corrupt-program problem rather than an application error.
Two IOError subclasses let you branch on the kind of network failure:
TimeoutError (a request or dial deadline was exceeded) and TlsError (TLS
handshake or certificate verification failed). Because they extend IOError,
catch (IOError e) still catches both; catch them specifically to, for example,
retry on a timeout but fail fast on a bad certificate:
try {
let r = http.get(url);
} catch (TimeoutError e) {
/* the request timed out - retry */
} catch (TlsError e) {
/* untrusted endpoint - do not retry */
} catch (IOError e) {
/* other connection failure (refused, DNS, reset) */
}
import io;
import net;
let listener = net.listenTcp("127.0.0.1:0");
let addr = net.localAddr(listener);
try {
let other = net.listenTcp(addr);
net.close(other);
} catch (IOError e) {
io.println("could not bind: " + e.message);
} finally {
net.close(listener);
}
Custom Error Classes
Define application error classes by extending Error or any built-in error
subclass:
class AppError extends Error {}
class NetworkError extends RuntimeError {}
try {
throw AppError("something failed");
} catch (NetworkError e) {
io.println("network: " + e.message);
} catch (AppError e) {
io.println("app: " + e.message);
} catch (Error e) {
io.println("other: " + e.message);
}
User error classes take an optional string message argument:
throw AppError(); # no message
throw AppError("connection refused"); # with message
Caught error values expose class and message fields:
} catch (AppError e) {
io.println(e.class + ": " + e.message);
}
Error class inheritance is fully hierarchical. A class extending RuntimeError
is also caught by catch (Error e):
class DatabaseError extends RuntimeError {}
class QueryError extends DatabaseError {}
try {
throw QueryError("syntax error near SELECT");
} catch (DatabaseError e) {
io.println("db problem: " + e.message);
}
Custom fields on error classes
Error classes can declare fields and a constructor. The constructor calls
parent(msg) to set the error message and then sets any custom fields on
this:
class HttpError extends RuntimeError {
int code;
func HttpError(int code, string msg) {
parent(msg);
this.code = code;
}
}
try {
throw HttpError(404, "page not found");
} catch (HttpError e) {
io.println(e.code as string); # 404
io.println(e.message); # page not found
}
Custom fields are accessible on the caught error by name just like message
and class.
Built-In Error Classes
| Class | Description |
|---|---|
Error |
Base class for all errors |
RuntimeError |
General runtime failures |
TypeError |
Type mismatch errors |
ValueError |
Invalid value errors |
IOError |
File and network I/O errors |
ParseError |
Parsing failures |
MatchError |
Non-exhaustive match |
AssertionError |
Failed assert(...) (1.6.0) |
ImmutableError |
Mutation of a frozen value (whole-class @immutable, @dataclass(frozen: true), or a set-once field after construction) (1.12.0) |
FatalError |
Unrecoverable fault; never caught (1.7.0) |
Catchable Errors vs FatalError (1.7.0)
Errors come in two tiers:
- Catchable
Error(and every class above it -RuntimeError,IOError,ValueError,TypeError,AssertionError, plus your own error classes). This includes implicit runtime faults: division by zero, index out of range, key-not-found, conversion failures like"abc".toInt(), and null access. They are caught bytry/catchidentically on the evaluator (geblang test) and the bytecode VM (geblang run/geblang build).
try {
let n = userInput.toInt(); # may fault on bad input
} catch (Error e) {
io.println("not a number: ${e.message}");
}
FatalErroris its own tier - it is not anError(x instanceof Erroris false) and notry/catchintercepts it, not evencatch (any e). It always unwinds to the top and terminates the program. It is reserved for unrecoverable conditions: raise one withthrow FatalError("message")when continuing would be meaningless. Stack-overflow (exceeding the maximum call depth) is also aFatalError.
if (configMissing) {
throw FatalError("config not found; cannot start");
}
The assert Builtin (1.6.0)
assert(cond) and assert(cond, message) are top-level builtins that
throw AssertionError when cond is false. They are the idiomatic
way to express "this must be true at this point in the program" - both
for debugging help and for shipped runtime invariants.
func transfer(int amount, Account from, Account to): void {
assert(amount > 0, "amount must be positive");
assert(from.balance >= amount, "insufficient funds");
from.balance = from.balance - amount;
to.balance = to.balance + amount;
}
The condition must be a bool, like every other Geblang condition.
When the optional message is omitted, the thrown error includes the
source text of the condition expression so failures are
self-describing:
assert(1 == 2);
# AssertionError: assertion failed: (1 == 2)
AssertionError is a direct subclass of Error, sibling to
RuntimeError, so it is catchable both specifically and generically:
try {
assert(invariantHolds());
} catch (AssertionError e) {
log.error("invariant violated: " + e.message);
}
Disabling at runtime
Both geblang <script> and geblang build accept a --no-assert
flag that elides every assert(...) call site at compile time. With
the flag set, the condition and the message are not evaluated, so
the call has truly zero runtime cost (and any side effects inside the
arguments are lost - assertions should never have side effects).
geblang --no-assert myapp.gb
geblang build --entry app.main --out app --no-assert
geblang test always runs assertions; the flag is intentionally not
honoured there.
The errors Module
import errors;
# Check error class hierarchy
let err = AppError("bad input");
io.println(errors.class(err)); # AppError
io.println(errors.message(err)); # bad input
io.println(errors.is(err, "Error")); # true
io.println(errors.is(err, "TypeError")); # false
# Create an error from a class name string
let dyn = errors.new("AppError", "dynamic");
# Wrap an error with additional context
let wrapped = errors.wrap(err, "request failed");
io.println(wrapped.message); # request failed: bad input
errors.is performs a full hierarchy-aware check. It traverses user-defined
class chains as well as the built-in error hierarchy.
Structured stack traces
Caught errors keep stack information. Use errors.stackTrace(e) or
e.stackTrace() to get an errors.StackTrace value instead of parsing the
formatted uncaught-error text yourself:
import errors;
import io;
func inner(): void {
throw RuntimeError("boom");
}
func outer(): void {
inner();
}
try {
outer();
} catch (RuntimeError e) {
let frames = e.stackTrace();
let first = frames.first();
io.println(errors.hasStackTrace(e)); # true
io.println(frames.length() > 0); # true
io.println(first.function()); # inner
io.println(first.line() > 0); # true
}
errors.StackTrace methods:
| Method | Returns | Description |
|---|---|---|
frames() |
list<errors.Frame> |
Structured frame values |
length() |
int |
Number of frames |
first() |
`errors.Frame | null` |
toList() |
list<dict> |
Plain dictionaries with function and line |
toString() |
string |
Original formatted stack text |
errors.Frame methods:
| Method | Returns | Description |
|---|---|---|
function() |
string |
Function name, or <top level> |
line() |
int |
Source line when available, otherwise 0 |
toDict() |
dict |
{function, line} |
toString() |
string |
Human-readable single frame |
Method frames are qualified with their declaring class as Class.method
(an inherited method reports the class that declared it, not the subclass
instance). Plain functions report their bare name.
For dictionary-oriented code, errors.frames(e) is shorthand for
errors.stackTrace(e).toList().
Runtime Failures
Most runtime failures, such as invalid operations, unknown fields,
bad argument types, and division by zero, are raised as catchable
RuntimeError exceptions - identically on the evaluator and the bytecode
VM (1.7.0). Parse, semantic, and startup failures are reported directly
because the script has not reached a recoverable runtime point;
stack-overflow and other unrecoverable conditions surface as
FatalError, which try/catch never intercepts.
Stack Traces
An error that escapes to the top level prints a classed header followed by one line per stack frame, identically on the evaluator and the bytecode VM (1.19.0):
uncaught ValueError: x too big: 7
at inner (line 6)
at middle (line 12)
at <top level> (line 15)
Frames are innermost first. The innermost frame shows the line where the
error happened; every caller frame shows the line of its call site; the
final <top level> frame shows where top-level code entered the chain.
Runtime faults use the same shape with the RuntimeError class. Method
frames are qualified as Class.method, anonymous functions render as
<closure>, and a deep tail-recursive run collapses into a single
at f (line 8) [x1000] entry instead of a thousand identical lines.
Caught errors expose the same information through errors.StackTrace and
errors.Frame, which is more stable for logging, testing, and tooling than
parsing the displayed uncaught-error output. Failing geblang test methods
report the same trace beneath the FAIL line.
When you convert low-level failures into domain errors, preserve the useful message:
func loadProfile(string path): dict<string, any> {
try {
return json.parse(io.readText(path));
} catch (Error e) {
throw RuntimeError("could not load profile " + path + ": " + e.message);
}
}