LEVIATHAN v0.1 · in development

Control & Errors

Errors & Resources

How Leviathan reports failure and cleans up after itself: throw/catch resolved by dynamic type, the standard exception hierarchy, the deliberate split between exceptions and T?/union returns, and using for deterministic resource teardown without a finally.

throw & catch

throw expr; is a control-transfer statement shaped like return — it carries a value out of the current point of execution instead of a normal result. The thrown value must implement IException: catchability is a contract, not "throw anything."

A try statement may list several catch clauses. Clauses are tried in source order and selected by the thrown value's dynamic type: the first clause whose declared type the value is assignable to wins — subclass → base class → implemented interface. The binding name in catch (Type name) is optional.

try {
    throw RuntimeException("Server error");
} catch (IRuntimeException e) {      // catch by CONTRACT (interface)
    console.writeln(e.message);
} catch (IException e) {             // fallback — first assignable clause wins
    console.writeln("unexpected: " + e.toString());
}

An exception that reaches the top of the program uncaught terminates execution with a report in the form Uncaught <Class>: <message>, and the process exits with status 1.

No finally

Leviathan has no finally clause. Scope-based cleanup (using) covers the common resource-teardown case; ordinary try/catch covers the rest. See Statements & Control Flow for the full try statement grammar.

The exception hierarchy

The standard hierarchy lives in namespace std, implicitly imported. Interfaces define catchability; classes carry the standard payload. The doubling is deliberate: any class can make itself catchable-as-X by implementing the interface, without inheriting the concrete class — multiple inheritance earning its keep.

NameKindExtends / implements
IExceptioninterfacerequires string message, string toString()
ExceptionclassIException
IRuntimeExceptioninterfaceIException
RuntimeExceptionclassException, IRuntimeException
ILogicExceptioninterfaceIException
LogicExceptionclassException, ILogicException
IException  (message, toString())
├─ Exception
│   ├─ RuntimeException  (also: IRuntimeException)
│   └─ LogicException    (also: ILogicException)
├─ IRuntimeException
└─ ILogicException

Naming is uniform — everything catchable ends in Exception (LogicException, not LogicError); Error is reserved in case uncatchable panics are ever introduced.

Runtime failures are loud. Built-in runtime failures throw real, catchable RuntimeExceptions rather than producing silent voids: an array index out of bounds, a Map.at call on an absent key (use has to test first), an unresolvable call target, or applying an operator a class doesn't define all throw instead of failing silently and distantly.

Errors as values

Exceptions are the primary error mechanism, but they are not the only one. For expected outcomes — absence, not malfunction — Leviathan prefers T? or union returns over throwing. This split is a core design stance: exceptions signal a programmer error or a genuine runtime fault; a nullable or union return signals an outcome the caller is expected to handle as ordinary control flow.

CallSignatureFailure mode
s.toInt()string -> int?malformed or overflowing text → None
s.toFloat()string -> float?malformed or non-finite text → None
json::parse(text)string -> JsonValue?malformed JSON → None (parse is total, never throws)
int?  p = "42x".toInt();     // None — trailing garbage, not a guess
int   n = p ?? 0;             // narrow with ?? or an explicit != None check

JsonValue? doc = json::parse(text);   // None on malformed input
if (doc != None) {
    string name = doc.at("user").at("name").asStr() ?? "?";
}

By contrast, indexing past the end of an array or calling Map.at on a missing key are programmer errors — the caller should have checked — so those throw a catchable RuntimeException instead of returning None. Anti-contagion is part of the rationale for favoring exceptions over a general Result<T, E>: a result type colors every signature up the call chain, the same contagion shape rejected for any and async-coloring, whereas exceptions keep ordinary signatures clean.

using: deterministic cleanup

using Type name = expr; declares a scope-owned resource. Type must be a reference type (not a struct) implementing IDisposableFile is the canonical prelude example. The binding is implicitly const (a using slot may never be reassigned), and it is legal only as a direct statement of a block — not a loop's one-statement body, not a field, not a global, and not a for..in binding (a v1 restriction).

close() runs exactly once on every way the declaring block can be exited: falling off the end, return, an uncaught throw unwinding past it, or a break/continue that leaves the block.

void copyLine(string path) {
    using File src = File(path, std::read);
    using File dst = File(path + ".bak", std::write);
    dst.writeln(src.readln());
}   // dst.close() runs, then src.close() — reverse declaration order, every exit edge

Multiple usings in one block close in reverse declaration order (last-in, first-closed) — the same discipline C#'s using and Python's context managers use. using composes with const (using const File f = ...; is a no-op, since using already implies it).

Dispatches on runtime class

close() dispatches on the binding's runtime class, so an interface-typed usingusing IDisposable d = makeResource(); — runs the concrete class's close() override, not the interface's empty requirement (which would silently skip teardown).

If close() itself throws while another exception is already unwinding, the new exception replaces the in-flight one — there is no finally-style chaining. If the block instead exits normally and close() throws, that exception propagates like any other.

IDisposable

The contract using requires, and the only thing it requires:

interface IDisposable { void close(); }

Prelude conformers: File (Streams, I/O & Network), TaskGroup (Concurrency & Async), and InStream<T>. User classes implement IDisposable the same way to opt a resource into using:

class PoolLease : IDisposable {
    private Pool owner;
    public void close() { owner.release(this); }
}

void borrow(Pool p) {
    using PoolLease lease = p.acquire();
    // lease.close() runs on every exit from this block
}