Errors and exceptions
throw passes any value to the nearest enclosing catch. If there is no catch, it unwinds the whole script.
throw and catch
throw expr can throw any value: a string, a number, a table, or an instance of a user-defined error class. There is no built-in Error type to inherit from.
try { ... } catch (e) { ... } binds e to the thrown value itself, with no wrapper and no conversion. typeof e after throw 42 is "integer". A catch clause may name a class before the bound name, catch (AmmoError e). It then matches only a thrown value that is instanceof that class. Several typed clauses may follow one try. They are tried in order. One untyped clause is allowed at the end as the catch-all.
class AmmoError {
reason = ""
constructor(reason) { this.reason = reason }
}
class NetworkError {}
function reload(ammoLeft) {
if (ammoLeft <= 0)
throw AmmoError("empty belt")
return ammoLeft - 1
}
// catch clauses are tried in order; a typed one matches instanceof-style,
// the untyped one is the catch-all and must come last
try {
reload(0)
} catch (AmmoError e) {
println("ammo problem:", e.reason)
} catch (NetworkError e) {
println("network problem")
} catch (e) {
println("other:", e)
}ammo problem: empty beltfunction reload(ammoLeft) {
if (ammoLeft < 0)
throw "negative ammo count"
if (ammoLeft == 0)
throw { code = "empty", ammoLeft = ammoLeft }
return ammoLeft - 1
}
try {
reload(-1)
} catch (e) {
println($"{typeof e}: {e}")
}
try {
reload(0)
} catch (e) {
println($"{typeof e}: {e.code}")
}string: negative ammo count
table: emptyNo finally, and rethrowing
Quirrel has try/catch but no finally. Cleanup that must run in both cases goes before the call that can throw, or in the catch block. In the catch block, throw e after the cleanup sends the same value on to an outer handler.
// there is no 'finally': cleanup that must always run goes right here,
// then a bare 'throw e' rethrows the same value onward
function deploySquad() {
println("releasing staging area lock")
try {
throw "landing zone is hot"
} catch (e) {
println("logged:", e)
throw e
}
}
try {
deploySquad()
} catch (e) {
println("mission control caught:", e)
}releasing staging area lock
logged: landing zone is hot
mission control caught: landing zone is hotassert
assert throws if its first argument is false (by the same rule as if: null, false, 0 and 0.0 are false). The default message is "assertion failed". A function as the second argument delays the message: assert calls it only when the assertion fails, and uses its return value as the thrown value. An expensive diagnostic then costs nothing when the assertion holds.
function describeSquad(squad) {
println("building diagnostic message")
return $"squad {squad.name} has {squad.strength} left"
}
let squad = { name = "alpha", strength = 4 }
// a plain string message
assert(squad.strength > 0, "squad has no strength left")
println("first assert passed")
// when the message argument is a function, it only runs if the assert
// fails, so an expensive message never costs anything on the happy path
assert(squad.strength > 0, @() describeSquad(squad))
println("second assert passed, describeSquad was never called")
try {
assert(squad.strength > 10, @() describeSquad(squad))
} catch (e) {
println("caught:", e)
}first assert passed
second assert passed, describeSquad was never called
building diagnostic message
caught: squad alpha has 4 leftUncaught errors
An error that reaches the top of the script without a matching catch unwinds every frame and passes the value to the host's error handler. The default handler of the standalone interpreter prints the thrown value, a call stack, and the locals of each frame, then stops the script. An embedding host installs its own handler and decides what to do with an uncaught error (log it, show it, ignore it).
A throw inside a script function that runs as a callback from native code (a sort comparator, an event handler) crosses that native frame. An ordinary try/catch around the call into native code catches it, the same as if no native code was involved.
// array.sort() is a native function that calls back into this comparator;
// a throw inside it crosses that native frame and reaches an ordinary
// try/catch around the call, exactly as if no native code were involved
let squads = [{ name = "alpha", hp = 5 }, { name = "bravo", hp = null }]
try {
squads.sort(function(a, b) {
if (a.hp == null || b.hp == null)
throw "squad with unknown hp"
return a.hp <=> b.hp
})
} catch (e) {
println("caught through sort():", e)
}caught through sort(): squad with unknown hpReading a "wrong type" error
The most common error is a type mismatch on a call. It has one form for a type-annotated parameter and for the argument check of a built-in function:
parameter 2 of 'heal' has an invalid type 'string' ; expected: 'integer'
The parameter index counts this as parameter 0, so parameter 1 is the first argument written at the call site, parameter 2 the second, and so on. expected lists every type the parameter accepts, |-joined for a union annotation such as int|null.
function heal(target, hp: int) {
target.hp += hp
return target.hp
}
let medic = { hp = 50 }
// this is the message people hit most: a type-annotated parameter
// (or a native function's own argument check) got the wrong type
try {
heal(medic, "ten")
} catch (e) {
println("caught:", e)
}
println("heal(medic, 10) =", heal(medic, 10))caught: parameter 2 of 'heal' has an invalid type 'string' ; expected: 'integer'
heal(medic, 10) = 60