Browse documentation

Errors

How GocciaScript reports errors to script authors, CLI users, and embedders.

Executive Summary

  • Error types -- Error, TypeError, ReferenceError, RangeError, SyntaxError, URIError, AggregateError, SuppressedError, plus the uncatchable resource ceilings TimeoutError (--timeout), InstructionLimitError (--max-instructions), and MemoryLimitError (--max-memory)
  • --max-memory refuses in two ways -- both force a collection and re-test first, and they differ in what the guest can do about the answer. A charged allocation (string payload, ArrayBuffer backing store) throws a catchable RangeError; a gated growth point (array element storage, object property storage) ends the run with the uncatchable MemoryLimitError. Either way, two shapes no collection could help are refused straight away without walking the heap -- a request larger than the whole budget, and a repeat of one a forced collection has already refused. The same byte count can therefore be survivable on one path and fatal on the other -- see Garbage Collector § Gated growth points
  • Engine-integrity faults -- A fourth uncatchable class, and not an error type at all: a use-after-free, an invalid dereference, or a broken heap unwinds past every catch to the host, because a script cannot meaningfully continue on top of one. See ADR 0109
  • Parser errors -- Displayed with source context, a caret pointing to the exact column, and optional suggestion text (e.g., "Use 'let' or 'const' instead")
  • Runtime errors -- Carry name, message, stack, and optional cause; catchable with try/catch/finally. A promise left rejected with no handler is reported as an uncaught error and fails the run
  • Sandbox filesystem errors -- Use real Error objects with Node-shaped code, errno, path, syscall, and optional dest metadata
  • Module loading errors -- Script-visible failures name only the specifier as written; the expanded host path reaches hosts through the typed exception and human-readable CLI output, never through error.message (the JSON envelope's error.fileName remains a host-side path, as it is for every error type)
  • JSON output -- --output=json wraps every execution result in a structured envelope with ok, error.type, error.message, error.line, and error.column. --output=compact-json produces the same envelope without the build, memory, stdout, or stderr fields, leaving only the normalized output array and structured error for console output. The same compact-json value is recognised by GocciaTestRunner (via --output) and GocciaBenchmarkRunner (via --format).
  • Error.cause -- All error constructors accept an options bag with a cause property for error chaining (ES2022+)

Error Types

GocciaScript supports the standard ECMAScript error constructors plus the CLI-only resource-ceiling types below. All JavaScript-visible error types inherit from Error and work with instanceof. TimeoutError, InstructionLimitError, and MemoryLimitError are CLI-only and not exposed as JavaScript constructors.

TypeThrown whenMDN
ErrorGeneric errors; base class for all error typesError
TypeErrorProperty access on null/undefined, calling a non-function, reassigning const, calling a constructor without newTypeError
ReferenceErrorAccessing an undeclared variable, using a variable before initialization (TDZ)ReferenceError
RangeErrorInvalid array length, negative ArrayBuffer size, out-of-range numeric conversions, call stack depth exceeded (--max-stack, or in bytecode mode the call stack reaching --max-memory)RangeError
SyntaxErrorInvalid syntax detected by the parser or lexer; also throwable at runtime via new SyntaxError(...)SyntaxError
URIErrorMalformed URI passed to encodeURI, decodeURI, encodeURIComponent, or decodeURIComponentURIError
AggregateErrorMultiple errors wrapped together; used by Promise.any when all promises rejectAggregateError
SuppressedErrorDisposal error during explicit resource management (using/await using); wraps both the new and suppressed errorSuppressedError
TimeoutErrorExecution exceeded the --timeout limit (CLI only; not a JS-visible constructor)--
InstructionLimitErrorExecution exceeded the --max-instructions budget (CLI only; not a JS-visible constructor)--
MemoryLimitErrorA gated growth point -- array element storage or object property storage -- was refused by the --max-memory budget after a forced collection failed to make room. Charged allocations under the same budget throw a catchable RangeError instead (CLI only; not a JS-visible constructor)--

The last three are resource ceilings rather than in-language errors. Script try/catch cannot observe them in either execution mode: they unwind past every handler to the host, because a ceiling the guest can catch is a ceiling the guest can ignore in a loop. Allocation sites that charge the budget against an owning value (string payloads, ArrayBuffer) still throw an ordinary catchable RangeError, which is unchanged.

One more class of failure is uncatchable, for a stronger reason and without appearing in the table at all: an engine-integrity fault -- a virtual call through a collected value, an invalid dereference, a heap whose bookkeeping has been destroyed. These are engine bugs rather than anything the script did, and unlike a refused allocation they have no defined continuation, so they unwind past every catch to the host instead of becoming an Error object. A refused allocation is deliberately not one of them: it leaves the heap intact, so catching it and retrying with less remains valid. ADR 0109 records the decision; source/units/Goccia.EngineFault.pas is the authoritative list of the classes.

Inheritance

All error types follow the standard prototype chain:

const e = new TypeError("bad type");
e instanceof TypeError; // true
e instanceof Error; // true
e instanceof RangeError; // false

Errors thrown internally by the engine (e.g., property access on null) use the same prototype chain as user-constructed errors, so instanceof checks work consistently in catch blocks.

Error.isError

GocciaScript implements the ES2026 Error.isError static method, which checks for the internal [[ErrorData]] slot:

Error.isError(new TypeError("x")); // true
Error.isError({ message: "fake" }); // false — plain objects are not errors

Parser Error Display

When the parser encounters invalid syntax, it displays a detailed error with source context. The format includes the error name, message, an optional suggestion, file location, the offending source line, and a caret (^) pointing to the exact column:

SyntaxError: Expected ";" after variable declaration
Suggestion: Add a ';' at the end of the statement
--> script.js:3:13
1 | const x = 1;
2 | const y = 2;
3 | const z = 3 4;
| ^
4 | console.log(z);

Up to 2 lines of context are shown before and after the error line.

Suggestions

Some parser errors include a suggestion line that recommends an alternative. GocciaScript keeps certain JavaScript forms off by default and uses suggestions to guide users toward the recommended alternatives:

SyntaxError: 'var' declarations are not supported in GocciaScript
Suggestion: Use 'let' or 'const' instead
--> script.js:1:1
1 | var x = 42;
| ^

The var and function messages still say "not supported" and don't yet name the --compat-var / --compat-function flag that turns the form on (#1270).

Other features that produce suggestions include:

  • var declarations -- suggests let or const
  • function declarations and expressions -- suggests arrow functions
  • == / != (loose equality) -- suggests === / !==
  • Traditional for(init; test; update) loops when --compat-traditional-for-loop is off -- suggests for...of, array methods, or the flag
  • for...in loops when --compat-for-in-loop is off -- suggests Object.keys() / Object.entries() with for...of, or the flag
  • while / do...while loops when --compat-while-loops is off -- suggests for...of, array methods, or the flag

See Language for the recommended profile, compatibility paths, and their rationale.

Runtime Error Display

When an uncaught runtime error reaches the top level, GocciaScript displays the error with the same source-context format used by parser errors. The stack trace from the error's stack property is used to locate the offending line in source:

TypeError: Cannot read properties of undefined (reading 'x')
Suggestion: check that the value is not null or undefined before accessing properties
--> script.js:2:21
1 | const getX = (obj) => {
2 | return obj.nested.x;
| ^
3 | };
4 | getX({});

A failed property read is located at the property whose read failed, where V8 reports it (see Stack Traces).

Runtime errors can carry a suggestion too. Reading or assigning an undeclared name prints Suggestion: check the spelling or declare the variable with const, let, or var before use in both modes, and the ReferenceError is always the realm's own, even when the global ReferenceError has been replaced. A RangeError from the call stack reaching --max-memory in bytecode mode is located at the call that needed the larger stack, as an ordinary stack overflow is.

Locations are not yet consistent between the two modes (#1273):

  • In interpreter mode, some top-level runtime errors lack a location. A top-level ReferenceError prints --> :0:0 with no file name or source excerpt, and a top-level TypeError such as const a = null; a.x; prints no --> line at all. Bytecode mode shows the file, line, and excerpt for both.

Stdin input uses <stdin> as the filename and retains full source context for error display. If source context is not available (e.g., errors originating inside native function callbacks without a JavaScript source location), GocciaScript falls back to displaying the stack trace string.

Unhandled promise rejections

A promise that is still rejected with no handler once the run has nothing left to do is an uncaught error (ES2026 §27.2.1.9 HostPromiseRejectionTracker). GocciaRunner prints the reason in the format above and exits 1, so Promise.reject(new Error("x")); and an un-awaited async call that throws both fail the run; with --output=json the reason is the envelope's error. A handler that arrives before the queue drains (a .catch attached from a later job, an await, a combinator such as Promise.all) cancels the report. When several promises are left, the first one rejected is reported; a rejected top-level await is an uncaught throw of its own and is reported ahead of them. A runScript child that leaves one fails with failureKind: "script-error", the test runner fails the test, hook, or file that left it, and the REPL reports it and keeps the session. Embedders choose with Engine.UnhandledRejections (Embedding); ADR 0124 records the decision.

Error Properties

Every error object has the following properties:

PropertyTypeDescription
namestringError type name (e.g., "TypeError", "RangeError")
messagestringHuman-readable error description
stackstringFormatted stack trace (see Stack Traces)
causeanyOptional; present only when constructed with { cause } option (see Error.cause)

AggregateError adds:

PropertyTypeDescription
errorsArrayThe array of errors passed to the constructor

SuppressedError adds:

PropertyTypeDescription
erroranyThe error that triggered the suppression
suppressedanyThe original error that was suppressed

Sandbox filesystem errors

The sandbox-mode fs module converts virtual filesystem failures into real JavaScript Error objects. Synchronous methods throw the error and fs.promises methods reject with the same shape. Callback methods pass the same error as their only callback argument, except fs.exists, which converts filesystem failures to false.

In addition to the standard error properties, sandbox filesystem errors have:

PropertyTypeDescription
codestringPortable error code such as "ENOENT"
errnonumberNegative, target-appropriate libuv-style error number
pathstringOriginal source path passed to the fs method
syscallstringStable sandbox operation name, such as "readFile" or "rename"
deststringOriginal destination path; present only for rename and copyFile

The sandbox operation name is independent of the API form: for example, readFileSync, fs.promises.readFile, and callback readFile failures all report syscall === "readFile". This identifies the virtual operation rather than claiming that the sandbox performed an operating-system call.

Virtual filesystem failurecodeMessage description
Invalid pathEINVALinvalid argument
Path not foundENOENTno such file or directory
Path already existsEEXISTfile already exists
Path component is not a directoryENOTDIRnot a directory
File operation targets a directoryEISDIRillegal operation on a directory
Directory is not emptyENOTEMPTYdirectory not empty
File or directory is busyEBUSYresource busy or locked
Sandbox byte quota exceededENOSPCno space left on device
Unexpected unclassified filesystem failure (internal fallback)EIOi/o error

Messages follow Node's system-error wording:

ENOENT: no such file or directory, readFile '/missing.txt'
ENOENT: no such file or directory, rename '/missing.txt' -> '/destination.txt'

code is the portable field to branch on. Numeric errno follows libuv's target convention, so its value can differ between operating systems.

Module loading errors

When a static or dynamic import fails against the host filesystem, the script-visible error message names only the specifier as the import statement wrote it:

try {
await import("./missing.js");
} catch (error) {
error.message; // 'Module not found: "./missing.js"'
}

The expanded host filesystem address is deliberately absent. Script code — including untrusted guest code — must not be able to map the host directory layout by importing probe specifiers, so no module-loading failure message reaching a JavaScript error object contains an expanded path, an applied alias replacement, or the importing file's base directory. This covers the whole load path, not resolution alone:

FailureScript-visible message
Specifier does not resolveModule not found: "./missing.js"
import.source of a non-script moduleModule source is not available for "./data.json"
JSON module fails to parseFailed to parse JSON module "./data.json": <parse detail>
JSON5, YAML, TOML, CSV, TSV or JSONL module fails to parseFailed to parse TOML module "./config.toml": <parse detail>
YAML module has no documentYAML module "./empty.yaml" must contain at least one top-level document.
Named import of a missing exportModule "./dep.js" has no export named "nope"

A cached module that changed on disk is reloaded through the request that reached it, so a reload that fails names that request's specifier too.

See ADR 0108 — Specifier-only module resolution errors.

Hosts keep the resolution diagnostic on the human-readable output path. The resolver raises the typed Pascal exception EModuleNotFound (Goccia.ModuleResolver), whose read-only ResolvedCandidatePath property carries the expanded candidate; the module loader forwards it on TGocciaModuleResolutionError (Goccia.Modules.Resolver), and the CLI reporters render it through FormatHostErrorDiagnostic as its own line:

RuntimeError: Module not found: "./missing.js"
--> entry.js:2:1
Resolved to: /home/user/project/missing.js

The --> line names the importing file, as its path was passed on the command line, at the import declaration that asked for the module. Both modes print this output.

A failed import() that nothing catches is located at the import() call, with a code frame:

Error: Module not found: "./nope.js"
--> main.js:2:9
1 | const load = async () => {
2 | await import("./nope.js");
| ^
3 | };
4 | load();

Both locations come from the call site that both modes enter while the load runs, the one a PermissionDenied from the same load reports. --output=json reports the same line and column.

--output=json and --output=compact-json do not carry it. The JSON envelope's error object is the documented set of type, message, line, column, and fileName, and its message is the same sanitized text script sees. Embedders that need the candidate path should catch EModuleNotFound or TGocciaModuleResolutionError directly rather than parse CLI JSON. The other two message shapes above carry no structured host counterpart at all — the expanded path survives only in the error's FileName field, which stays host-side: the --> line of human-readable output and the envelope's error.fileName, never error.message.

Two resolution messages are unaffected because they never carried a host path: a bare specifier reports Cannot resolve bare module specifier "lodash". Imports must start with "./" or "../", and a runtime configured without a resolver reports No module resolver configured and cannot resolve "./missing.js".

Sandbox modules are exempt. Sandbox mode resolves against a virtual filesystem the guest already owns and can enumerate, so its resolution failures keep the (resolved to "...") and (alias resolved to "...") detail — those paths are guest namespace, not host namespace.

No content provider configured

An engine that was never given a module content provider refuses every module load it is asked to retrieve. The refusal is a JavaScript error rather than an RTL exception: import() rejects with it and source can catch it, and a static import — which no try/catch in the importing module can wrap — surfaces it as a JavaScript throw (TGocciaThrowValue) carrying the same error value.

Loading a module is resolution followed by retrieval, and only retrieval reaches the provider. A specifier the resolver rejects fails first, and that is a different failure carrying no code — with the default resolver, which resolves against the host filesystem, import "./dep.js" where dep.js does not exist fails there. import() rejects with a plain Error reading Module not found: "./dep.js", and a static import raises TGocciaRuntimeError out of the engine as a host-language exception. So an embedder still has to guard its engine boundary. Resolution messages name only the specifier as written (see above), matching the refusal below.

In addition to the standard error properties, the refusal carries:

PropertyTypeDescription
codestring"ERR_MODULE_LOADING_UNSUPPORTED"
try {
await import("./dep.js"); // resolves, no provider installed
} catch (error) {
error instanceof Error; // true
error.name; // "Error"
error.code; // "ERR_MODULE_LOADING_UNSUPPORTED"
error.message; // "Cannot load module: no module content provider is configured"
error.path; // undefined — see below
}

There is deliberately no path, and the message names no module address. The only address available where the refusal is raised is the resolved one, which the default resolver expands into an absolute host filesystem path, and an engine with no provider is exactly the configuration an embedder runs untrusted source in — an enumerable own property would carry that host detail into JSON.stringify(error) and object spread. Nothing is lost: while no provider is installed the refusal is unconditional for every module, so acting on it never depends on which one was asked for. Contrast sandbox filesystem errors, whose path is a sandbox VFS address the guest itself named.

The error type is a plain Error rather than a TypeError. ECMA-262 HostLoadImportedModule requires a throw completion but mandates no type, and for this case — an engine with no loader at all — V8, JavaScriptCore, and SpiderMonkey all report a plain Error; TypeError is the convention for a configured loader that tried and failed. Because the constructor cannot separate the two, code is the field to branch on: a provider that is configured and cannot produce a module reports its own failure and never ERR_MODULE_LOADING_UNSUPPORTED. See ADR 0106 for the full rationale, and Embedding for how to install a provider.

Stack Traces

The stack property contains a V8-style formatted string with the error header followed by at frames. Each frame shows the function name (or <anonymous>), the file path, and the line and column number. Frames are listed from innermost (most recent) to outermost.

const inner = (obj) => {
return obj.x;
};
const middle = (obj) => {
inner(obj);
};
const outer = () => {
middle(null);
};
try {
outer();
} catch (e) {
console.log(e.stack);
}

In bytecode mode this prints:

TypeError: Cannot read properties of null (reading 'x')
at inner (script.js:2:14)
at middle (script.js:5:3)
at outer (script.js:8:3)
at <module> (script.js:11:3)

The first frame is where the error happened. Every other frame is at the call that frame is making: middle at its call to inner on line 5, and <module>, the script's top level, at its call to outer. Node prints the same positions; it names the top-level frame Object.<anonymous>.

Positions follow V8's rules, in both modes:

ConstructLocated atExample
A call whose ( follows an identifierThat identifier: the callee, or the property name of a member calleeinner(obj) at inner, list.map(f) at map, super() at super
Any other callIts (obj["m"](), (f)(), f()(), f?.(), promise.catch(f), this.#m()
newThe new keywordnew ns.Widget() at new
A tagged templateThe templatetag`text` at the backtick
A failed property readThe property name, or the [ of a computed readobj.x at x, obj["x"] at [

V8 decides a call's position by the token before its (, so a reserved-word property name such as catch, default or delete, and a private name, leave the call at its (. The same position is what an audit event and a PermissionDenied error report for the call. Interpreter mode lists no <module> frame, and each frame there shows the site where that function was called (middle at 8:3, inside outer) rather than its own call to the next frame (#1273).

A trace lists at most 100 frames, the innermost ones. A deeper stack ends with one ... N more frames line, indented like the at lines (... 1 more frame for one), so a stack-overflow RangeError still shows where the recursion is: at the default --max-stack of 2,200, const f = () => { f(); }; f(); gives the header, 100 at f lines and ... 2101 more frames. Frames past the limit are neither named nor formatted; only their line and column numbers are read, as bytecode mode works out every frame's position from the instruction pointers the VM saved. Without the limit, building the trace of a stack-overflow RangeError took most of a deep recursion's run time. A stack of up to 100 frames renders as it always has. The limit applies to every error that captures a stack, in both modes, and is fixed (STACK_TRACE_FRAME_LIMIT in Goccia.CallStack); GocciaScript has no Error.stackTraceLimit, which is a V8 extension. JavaScriptCore's default Error.stackTraceLimit is also 100, SpiderMonkey keeps 128 frames and V8 10.

Error.cause

Most error constructors accept an options object with a cause property, following ES2022 Error Cause. The options argument position varies by constructor: second for Error, TypeError, RangeError, ReferenceError, SyntaxError, and URIError; third for AggregateError (new AggregateError(errors, message, options)); fourth for SuppressedError (new SuppressedError(error, suppressed, message, options)).

const original = new Error("disk full");
const wrapped = new Error("save failed", { cause: original });
​
wrapped.message; // "save failed"
wrapped.cause.message; // "disk full"

Error cause chaining works across error types:

const root = new RangeError("out of bounds");
const mid = new TypeError("invalid type", { cause: root });
const top = new Error("operation failed", { cause: mid });
​
top.cause.cause.message; // "out of bounds"

The cause property:

  • Can be any value (string, number, object, another error, null, undefined)
  • Is writable and configurable but not enumerable
  • Is only present when the options object has a cause property (not present by default)
  • Works with all error types: Error, TypeError, RangeError, ReferenceError, SyntaxError, URIError, AggregateError, and SuppressedError

try / catch / finally

Error handling follows standard ECMAScript semantics. See Language for the full syntax reference.

Basic try/catch

try {
const x = null;
x.property; // throws TypeError
} catch (e) {
console.log(e.name); // "TypeError"
console.log(e.message); // "Cannot read properties of null (reading 'property')"
}

Optional catch binding

The catch parameter can be omitted (ES2019+):

try {
riskyOperation();
} catch {
console.log("something went wrong");
}

finally

The finally block always runs, whether or not an error was thrown:

try {
return computeResult();
} finally {
cleanup(); // runs even when try returns
}

Per the spec, if finally contains a return, throw, or break, it overrides the try/catch result.

Throwing any value

GocciaScript allows throwing any value, not just error objects:

try {
throw "string error";
} catch (e) {
console.log(e); // "string error"
}
​
try {
throw 42;
} catch (e) {
console.log(e); // 42
}

Type-checking caught errors

Use instanceof to differentiate error types in a catch block:

try {
someOperation();
} catch (e) {
if (e instanceof TypeError) {
console.log("type error:", e.message);
} else if (e instanceof RangeError) {
console.log("range error:", e.message);
} else {
throw e; // rethrow unknown errors
}
}

SuppressedError and Explicit Resource Management

When using using or await using declarations for explicit resource management, if both the block body and a resource's [Symbol.dispose]() method throw, the runtime wraps both errors in a SuppressedError:

let caught;
try {
using resource = {
[Symbol.dispose]() { throw new Error("disposal failed"); }
};
throw new Error("block failed");
} catch (e) {
caught = e;
}
​
caught instanceof SuppressedError; // true
caught.error.message; // "disposal failed"
caught.suppressed.message; // "block failed"

If multiple disposals fail, errors are chained: each new disposal error wraps the previous SuppressedError as its suppressed property.

SuppressedError can also be constructed directly:

const err = new SuppressedError(
new Error("new"), // error
new Error("old"), // suppressed
"An error was suppressed" // message (optional)
);
err.name; // "SuppressedError"
err.error; // Error: new
err.suppressed; // Error: old
err.message; // "An error was suppressed"

JSON Output Format

--output=json and --output=compact-json wrap every run in a structured JSON envelope; its fields, including how an error and a timeout are reported, are described in JSON Output.

Embedding

For Pascal-side error handling when embedding GocciaScript in FreePascal applications, see Embedding the Engine. The engine raises TGocciaError subclasses (TGocciaSyntaxError and its subclass TGocciaLexerError, TGocciaTypeError, TGocciaReferenceError) on the Pascal side and TGocciaThrowValue for JS-level throw statements.

  • Language -- Implemented features, recommended defaults, compatibility paths, and rationale
  • Built-in Objects -- Error constructors and API reference
  • Embedding the Engine -- Pascal-side error handling for embedders
  • Testing -- Writing tests that assert on error behavior
  • JSON Output -- The --output=json envelope and its error object