try, catch and runtime errors
try, catch, finally
You throw and catch C++ exception types. A finally block always runs. A script can declare an exception type (see “Script exception types”). It can also catch the builtin types NullReference, IndexOutOfRange, ArithmeticError, NativeError, ScriptError, TaskCancelled and TaskReloadCancelled (see “Runtime errors”).
class Safe {
int Div(int x, int y) {
try {
if (y == 0) throw std.runtime_error("divide by zero");
return x / y;
} catch (std.exception e) {
std.printf("error: %s\n", e.what());
return 0;
} catch (...) {
throw;
} finally {
std.printf("done\n");
}
}
}
Runtime errors
The runtime checks a pointer, a handle and an index of an array, a std::vector or any type with the stub attribute checked_index (see Native interop). A failed check throws one of these builtin types:
NullReference: the script used a null pointer or a null handle, for examplep.xor*p. A null handle, a nullT*or an expiredCheckedPtrpassed to a nativeT&orconst T¶meter also throws it. This check and the checked unwrap(T*)pare always on, also withHOT_RUNTIME_CHECKS=0. See Limits.IndexOutOfRange: an index is negative or not less than the size of an array, astd::vectoror a checked native container.ArithmeticError: an integer/,%,/=or%=had a zero divisor. The message isinteger divide by zeroorinteger modulo by zero. The check is always on, also withHOT_RUNTIME_CHECKS=0.INT_MIN / -1does not throw: it wraps toINT_MIN, andINT_MIN % -1is0. A float divide by zero givesinfornanand does not throw.ArithmeticErroralso covers a shift:<<,>>,<<=or>>=with a negative count, or with a count not less than the bit width of the promoted left operand. The message isshift count out of range. The check is always on. A left shift wraps and does not throw. A right shift of a negative value copies the sign bit.IndexOutOfRangealso coversfront(),back(),top(),pop(),pop_back()andpop_front()on an empty container (vector, deque, list, string, queue, stack,priority_queueand each type withempty()). The message isindex 0 is out of range for an array of size 0. The check is always on.m.at(k)with a missing key throwsScriptErrorwith the messagekey not found: ....v.at(i)with a bad index throwsIndexOutOfRange.NativeError: a native exception that is not aScriptError(std::out_of_range,std::bad_optional_access,std::bad_alloc, an exception of the host). Atryblock with acatch (ScriptError e)orcatch (NativeError e)clause converts it. The message isnative exception <type>: <what>. The conversion is off when thetryhas a clause for...or for a native type (std.exception,std.out_of_range). Then the clauses see the original exception, and aScriptErrorclause does not catch a native exception. An exception that no script code handles goes to the script error handler with its demangled type name.ScriptError: the base of the types above. A call that is too deep throws aScriptErrorwith the messagestack overflow. See Limits.TaskCancelledandTaskReloadCancelled: a task ends without a result. See Async and await.
Write the type name in a catch clause. The prefix hot. is also valid. Use catch (ScriptError e) to catch all of them. e.what() gives the message.
class Grid {
int[4] vals;
Weapon weapon = null;
int At(int i) {
try {
return this.vals[i];
} catch (IndexOutOfRange e) {
return -1;
}
}
int Dmg() {
try {
return this.weapon.dmg;
} catch (ScriptError e) {
std.printf("error: %s\n", e.what());
return 0;
}
}
}
A throw does not roll back. A field keeps each value that the script wrote before the throw. A script that does not catch the error passes it to the host. See Error reports. The host can remove the checks at build time (HOT_RUNTIME_CHECKS in CMake). Then a null access or a bad index is undefined behavior.
Script exception types
A struct with the base ScriptError is an exception type. The base can also be written hot.ScriptError, or it can be another exception struct. No new syntax is needed.
struct GameError : ScriptError {
int code = 0;
GameError(int c) : base("game error") { code = c; }
}
struct NetError : GameError {
NetError(int c) : base(c) { }
}
class Player : Actor {
int last = 0;
void Run() {
try {
throw NetError(7);
} catch (GameError e) {
last = e.code;
std.printf("%s\n", e.what()); // game error
}
}
}
throw E(args)throws a value of the struct.base("text")in a constructor sets the message.e.what()gives it. A constructor that has nobase(...)call uses the name of the first struct in the chain whose base isScriptErroras the message. A child struct withoutbase(...)gets the name of its parent.base(...)of a struct with the baseScriptErrortakes exactly one message argument. A child struct passes itsbase(...)arguments to the constructor of the parent struct.catch (E e)catchesEand each struct that derives from it. The variable is read-only.- A script error that nothing catches goes to the host. The report shows the type
hot::ScriptError, with the message of the exception. - A script class (not a struct) is not an exception type.
throw new $C()is an error:cannot throw the script class 'C': declare 'struct C : ScriptError' for an exception.
An exception struct is for throw and catch only. It cannot be the type of a field, a module variable or an element of a container such as std.vector<E>. A reload moves the state of a field with the Nested migration, and this migration does not keep the message of the base. A local variable and a parameter can have the type.
Change of a container in a foreach
A change of the shape of a container while foreach (var x in c) walks it is undefined behavior in C++. The translator reports a compile error for such a change in the loop body. The shape changes are c = ..., and a call of push_back, insert, erase, clear, resize, emplace, pop_back and the other methods that can move or remove elements. Collect the changes in a new list, and apply them after the loop. A write to an element (c[i] = v, x.field = v) and a change of a different container are allowed. The check has limits. See Limits.
Checks against crashes
Each row shows how HotScript stops one cause of a crash. The mechanism is a compile error (the translator rejects the script), a runtime exception (a recoverable ScriptError), or unchecked. What can still crash lists the unchecked cases.
| Cause | Mechanism | Exception | Test |
|---|---|---|---|
| Null handle, pointer or optional | Compile error where the analysis sees null. Else runtime. | NullReference |
golden 55-59; NullDerefCaughtInScript; StarNullCaught; RuntimeChecks.DerefOfNullPointerThrows, DerefOfAnEmptyOptionalThrows |
| Null interface | Runtime | ScriptError |
CallOfANullInterfaceThrowsAScriptError; Fixture.NullHandleThrows |
Null or expired CheckedPtr |
Runtime, always on | NullReference |
CheckedPtr.ArrowOnNullThrows, ArrowOnExpiredThrows; CheckedPtrFieldReadsNullAfterTheTargetDies |
Null handle to a native T& |
Runtime, always on | NullReference |
NullHandleToNativeReferenceParameterIsCatchable; NullAlwaysOn.* |
| Index out of range: vector, array, span, string | Runtime | IndexOutOfRange |
IndexOutOfRangeCaughtInScript; ViewParameterConvertsAndChecksIndex; RuntimeChecks.*IndexOutOfRangeThrows |
| Missing key in a map | Runtime, at only |
IndexOutOfRange or NativeError |
EmptyContainerOperationsAndNativeExceptionsAreCatchable; RuntimeChecks.IndexOfMapUsesItsOperator |
Empty front, back, pop |
Runtime, always on | IndexOutOfRange |
EmptyContainerOperationsAndNativeExceptionsAreCatchable; container_ops_tests.cpp |
Dangling ref |
Compile error | None | golden 27, 28, 82-91, 94-103, 108 |
| Dangling view | Compile error | None | golden 105-107 |
Container change in foreach |
Compile error | None | golden 92, 93, 104 |
| Use after reload | A handle stays valid. A task of the old code is cancelled. | TaskReloadCancelled |
AsyncHost.ReloadCancelsTheSuspendedTask; TaskReload.*; CheckedPtrFieldKeepsItsTargetAcrossReload |
| Use after a native free | CheckedPtr and handles are checked. A raw T* is unchecked. |
NullReference |
CheckedPtrFieldReadsNullAfterTheTargetDies |
| Integer divide or modulo by zero | Runtime, always on | ArithmeticError |
IntegerDivideAndModuloAreChecked; arith_tests.cpp |
| Shift out of range | Runtime, always on | ArithmeticError |
ShiftsOverflowAndFloatCastsAreDefined; arith_tests.cpp |
| Signed overflow, float to int cast | Defined result (-fwrapv, saturation) |
None | SignedOverflowWrapsWithTheOptimizer; ShiftsOverflowAndFloatCastsAreDefined |
| Deep recursion | Runtime, always on | ScriptError("stack overflow") |
InfiniteRecursionIsCaughtInScript; RecursionLimitIsExactAndCanChange |
Bad as or derived cast |
Gives a null handle. The use is checked as null. | NullReference |
BadAsAndDerivedCastGiveNullAndTheUseThrows; IsAs.ScriptClassesWalkTheParentRecords; CheckedUnwrapCastThrowsOnNull |
await on a dead task |
Runtime | ScriptError (TaskCancelled for a cancel) |
TaskTests.EmptyTaskAwaitThrows, DestroyOfAnAwaitedChildGivesTheParentAnError; TaskReload.AwaitOfATaskCreatedBeforeAReloadGetsTheError |