Repository navigation
Errors and Debugging
CupidScript has two major error classes:
- Parse errors (lexer/parser)
- Runtime errors (VM)
When parsing fails, the parser produces an allocated error string like:
Parse error at <input>:line:col: message
Examples:
- "expected expression"
- "expected ')'"
- "expected '{'"
- "expected function name"
- "invalid assignment target"
Runtime errors include source location:
Runtime error at source:line:col: message
Common runtime errors:
- undefined variable
- type error: arithmetic expects int
- division by zero
- mod by zero
- comparisons require both ints or both strings
- attempted to call non-function
- wrong argument count
- indexing expects list[int] or map[key]
- field access expects map
- uncaught exception (from
throw)
CupidScript supports exception handling via throw, try/catch, and optional finally:
try {
if (x < 0) {
throw error("Invalid input: negative number", "INVALID_ARG");
}
process(x);
} catch (e) {
if (is_error(e)) {
print("Error caught:", e.msg);
} else {
print("Error caught:", e);
}
} finally {
// cleanup that should always run
print("cleanup");
}Thrown values can be any type - commonly strings or maps with error details:
throw {"msg": "file not found", "code": 404, "path": p};For standardized error handling, use the error() helper to create error objects:
-
error(msg)- Creates a map with{msg, code, stack}fields (codedefaults to"ERROR") -
error(msg, code)- Creates a map with{msg, code, stack}fields -
is_error(value)- Checks if value is an error object (hasmsgandcodefields) -
format_error(err)- Formats an error object into a readable string ([CODE] message)
There is also a global ERR map containing common string error codes you can use with error(msg, code).
The stack field contains a list of stack frame strings captured at the time of error creation.
Example:
fn divide(a, b) {
if (b == 0) {
throw error("Division by zero", "DIV_ZERO");
}
return a / b;
}
try {
let result = divide(10, 0);
} catch (e) {
if (is_error(e)) {
print("Error:", e.msg);
print("Code:", e.code);
print("Formatted:", format_error(e));
print("Stack trace:");
for frame in e.stack {
print(" ", frame);
}
} else {
print("Caught:", e);
}
} finally {
// release resources or reset state
print("cleanup done");
}Benefits of error objects:
- Consistent structure across all errors
- Automatic stack trace capture
- Error codes for categorization (
codeis always present) - Easy to check with
is_error()
catch handles explicit throw values and rejected promises. Runtime errors such as division by zero still abort execution, but finally runs before the error reaches the host. A return or throw from cleanup replaces the earlier result; a new runtime error replaces the earlier diagnostic. Host instruction, timeout, and interrupt limits also apply to cleanup.
finally can be a block or a single expression statement:
try { do_work(); } catch (e) { print(e); } finally cleanup();Your VM maintains a call frame stack for:
- native function calls
- user-defined function calls
- dotted "method/global fallback" calls
When an error occurs, the VM appends:
Stack trace:
at func (file:line:col)
at ...
This makes it practical to debug scripts without a debugger attached.
- When building larger scripts, wrap assumptions with
assert(...). - Use
typeof(x)when debugging dynamic type issues. - Use
fmt("%v", x)orprint(x)to inspect values.