Handling errors
editCatch the errors of a script
An error that a script does not catch is thrown to .NET as a JsException:
using var engine = new JsEngine();
var realm = engine.CreateRealm();
var exception = Assert.Throws<JsException>(() => realm.Execute("function check(order) {\n if (!order.id) throw new RangeError('The order has no id');\n}\ncheck({});", "orders.js"));
// The thrown value, and where it was thrown
Assert.Equal("RangeError: The order has no id", exception.Message);
Assert.Equal(new JsSourceLocation("orders.js", 2, 18), exception.Location);
using (engine.Enter())
{
var error = exception.Value.AsObject();
Assert.Equal("RangeError", error.GetProperty("name").AsString());
Assert.Equal("The order has no id", error.GetProperty("message").AsString());
Assert.Equal("RangeError: The order has no id\n at check (orders.js:2:28)\n at orders.js:4:1", error.GetErrorStack());
}
// A script can throw any value
exception = Assert.Throws<JsException>(() => realm.Execute("throw 42;"));
Assert.Equal(42, exception.Value.AsNumber());- Value is the thrown value. A script can throw any value, not only errors: check its kind before you read it.
Messagedescribes the thrown value without running code of the script: thenameand themessageof an error (RangeError: The order has no id), orUncaughtand the value for anything else (Uncaught 42).- Location is the position of the
throw(or of the operation that failed), with the URL given to Execute. Lines and columns are 1-based. - GetErrorStack returns the
stackof an error object. Its first frame is where the error object was created (new RangeError(...)), which can differ from the position of thethrow.
The same exception comes from JsEngine.Call, JsEngine.Construct, and from the conversions that run code of the script, such as engine.ToString calling a toString method that throws.
Read the JavaScript stack trace
The StackTrace of a JsException is the one of .NET. JavaScriptStackTrace gives the frames of the script, one per line, the innermost first, and ToString() includes them for your logs:
using var engine = new JsEngine();
var realm = engine.CreateRealm();
var exception = Assert.Throws<JsException>(() => realm.Execute("""
function parse(text) {
throw new SyntaxError('Unexpected ' + text);
}
function load() {
return parse('}');
}
load();
""", "loader.js"));
// The JavaScript frames, the innermost first: the first one is where the error was created
Assert.Equal("""
at parse (loader.js:2:13)
at load (loader.js:5:10)
at loader.js:7:1
""", exception.JavaScriptStackTrace);- The frames are the ones of the code that created the error object, which can differ from the code that threw it.
- It is null when the thrown value is not an error with a stack trace, such as
throw 42: use Location then. It is empty for an error created when no script runs. - A stack trace has at most 10 frames, or
Error.stackTraceLimitframes (see below).
Error.captureStackTrace
Some npm packages call Error.captureStackTrace, the stack trace API of V8, to give a stack to their own error objects. It is not part of ECMAScript, so it is disabled by default; EnableV8StackTraceApi adds it to the realms:
using var engine = new JsEngine(new JsEngineOptions { EnableV8StackTraceApi = true });
var realm = engine.CreateRealm();
var stack = realm.Execute("""
class HttpError {
constructor(status) {
this.name = 'HttpError';
this.message = 'Status ' + status;
// Omit the frames of the constructor
Error.captureStackTrace(this, HttpError);
}
}
function request() { return new HttpError(404); }
request().stack
""", "http.js").AsString();
Assert.Equal("HttpError: Status 404\n at request (http.js:9:33)\n at http.js:10:1", stack);Error.captureStackTrace(object, constructorOpt)defines thestackproperty of any object: writable, configurable and not enumerable. The frames above the innermost call ofconstructorOpt, and this call, are left out; whenconstructorOptis not on the stack, there is no frame. The first line isname: message, asError.prototype.toStringbuilds it.Error.stackTraceLimitsets the number of frames of the stack traces of the realm, including the ones of the errors created afterwards. It is 10 by default, and a value that is not a number gives no frame.Error.prepareStackTraceis not supported: the stack traces are always strings.
Map errors to the original sources
A script generated by a bundler or a compiler (TypeScript...) throws errors at positions of the generated code. A JsSourceMapper maps them to the original sources with the source maps of the scripts:
// A bundle generated from src/check.ts and src/run.ts, and its source map
var bundle = """
function check(order) {
if (!order.id) throw new RangeError('The order has no id');
}
function run() { check({}); }
run();
""";
var sourceMap = JsSourceMap.Parse("""
{
"version": 3,
"file": "bundle.js",
"sourceRoot": "src",
"sources": ["check.ts", "run.ts"],
"mappings": "AAAA;AAGA,EAAE;AAEF;ACIA,iBACE;AAEF"
}
""", "dist/bundle.js.map");
var mapper = new JsSourceMapper();
mapper.Add("bundle.js", sourceMap);
using var engine = new JsEngine();
var realm = engine.CreateRealm();
var exception = Assert.Throws<JsException>(() => realm.Execute(bundle, "bundle.js"));
// The positions in the original sources
Assert.Equal(new JsSourceLocation("bundle.js", 2, 18), exception.Location);
Assert.Equal(new JsSourceLocation("dist/src/check.ts", 4, 3), mapper.MapLocation(exception.Location));
using (engine.Enter())
{
var stack = exception.Value.AsObject().GetErrorStack();
Assert.Equal("RangeError: The order has no id\n at check (dist/src/check.ts:4:3)\n at run (dist/src/run.ts:11:3)\n at dist/src/run.ts:13:1", mapper.MapStackTrace(stack!));
}- JsSourceMap.Parse reads a source map of version 3, including the index maps (
sections). It throws aFormatExceptionfor a map that is not valid; TryParse returns false instead. Give it the URL of the map: the relative URLs of the sources (aftersourceRoot) are resolved against it. - A source map belongs to the URL of the generated script, the URL given to
realm.Executeorrealm.ParseModule. Add it with Add, or give the constructor a resolver that loads the map of a URL on first use (for instance from the//# sourceMappingURL=comment of the script) and returns null when there is none. - MapLocation maps JsException.Location; MapStackTrace maps the frames of a stack trace, such as the
stackof an error or the result ofengine.CaptureStackTrace(). A position is mapped to the start of the mapping that contains it. A position without source map, or that no mapping contains, is kept. - The mapping is done by your code when it reports an error: the scripts still see the generated positions in the
stackof their errors.
The exceptions of the engine
| Exception | When |
|---|---|
| JsException | A script threw a value that it did not catch, or the code is not valid (a SyntaxError). |
| JsSyntaxErrorException | JsPreparedScript.Parse got a script that is not valid. See Scripts. |
| JsTerminationException | A limit stopped the script, or it was canceled. See Limits and sandboxing. |
Throw a JavaScript error from .NET
A .NET function called by a script throws a JavaScript error with realm.CreateException. The script can catch it like any error:
using var engine = new JsEngine();
var realm = engine.CreateRealm();
using (engine.Enter())
{
realm.GlobalObject.SetProperty("parseAge", realm.CreateFunction("parseAge", 1, (_, arguments) =>
{
var text = arguments.Length > 0 ? engine.ToString(arguments[0]) : "";
if (!int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out var age))
{
// A JavaScript error, which the scripts can catch
throw realm.CreateException(JsErrorType.TypeError, "Invalid age: " + text);
}
return age;
}));
}
Assert.Equal(42, realm.Execute("parseAge('42')").AsNumber());
Assert.Equal("TypeError: Invalid age: abc", realm.Execute("try { parseAge('abc'); } catch (e) { `${e}` }").AsString());- JsErrorType gives the native error types: Error, TypeError, RangeError, SyntaxError, ReferenceError, EvalError and UriError. Prefer the type that a built-in would throw: TypeError for an argument of the wrong type, RangeError for a value out of range.
realm.CreateErrorcreates the error object without throwing it, for instance to reject a promise.- To throw any other value, throw
new JsException(value). new JsException("message")throws anErrorwhosemessageis the message of the exception. The error is created in the current realm when the script catches it. Userealm.CreateExceptionfor the other error types.
Exceptions that are not JavaScript errors
Any other .NET exception thrown by a .NET function is not a JavaScript error. The script cannot catch it: it goes through the script, without running its catch blocks, to the code that called Execute:
using var engine = new JsEngine();
var realm = engine.CreateRealm();
using (engine.Enter())
{
realm.GlobalObject.SetProperty("fail", realm.CreateFunction("fail", 0, (_, _) => throw new InvalidOperationException("Unexpected state")));
}
// A .NET exception is not a JavaScript error: the script cannot catch it, and it goes to the caller
Assert.Throws<InvalidOperationException>(() => realm.Execute("try { fail(); } catch { }"));Convert the failures that a script should handle, such as invalid input, to JavaScript errors. Let the other ones propagate: they are bugs or failures of your application.
Like a JsTerminationException, such an exception leaves the code it goes through, without running its catch and finally blocks, and the engine can run code afterwards:
- A generator that it stops is completed: its next
next()call returns{ done: true }. - An async function or an async generator that it stops is completed, and its promise (the promise of the current request for an async generator) is rejected with an error such as
Error: The execution was stopped by a .NET exception: System.InvalidOperationException: Unexpected state, so the code that awaits it does not wait forever. When the exception is thrown by the continuation of anawait, it goes to the caller of RunJobs; the other jobs stay in the queue. - A module that it stops is evaluated with this error. See Modules.