diff --git a/src/transformation/utils/scope.ts b/src/transformation/utils/scope.ts index 314f1d795..e1a66fdf2 100644 --- a/src/transformation/utils/scope.ts +++ b/src/transformation/utils/scope.ts @@ -39,8 +39,8 @@ export interface Scope { loopContinued?: LoopContinued; functionReturned?: boolean; asyncTryHasReturn?: boolean; - asyncTryHasBreak?: boolean; - asyncTryHasContinue?: LoopContinued; + tryHasBreak?: boolean; + tryHasContinue?: LoopContinued; } export interface HoistingResult { @@ -96,7 +96,7 @@ export function findAsyncTryScopeInStack(context: TransformationContext): Scope } /** Like findAsyncTryScopeInStack, but also stops at Loop boundaries. */ -export function findAsyncTryScopeBeforeLoop(context: TransformationContext): Scope | undefined { +export function findTryScopeBeforeLoop(context: TransformationContext): Scope | undefined { for (const scope of walkScopesUp(context)) { if (scope.type === ScopeType.Function || scope.type === ScopeType.Loop) return undefined; if (scope.type === ScopeType.Try || scope.type === ScopeType.Catch) return scope; diff --git a/src/transformation/visitors/break-continue.ts b/src/transformation/visitors/break-continue.ts index e41a7934b..0d0a21061 100644 --- a/src/transformation/visitors/break-continue.ts +++ b/src/transformation/visitors/break-continue.ts @@ -2,13 +2,12 @@ import * as ts from "typescript"; import { LuaTarget } from "../../CompilerOptions"; import * as lua from "../../LuaAST"; import { FunctionVisitor } from "../context"; -import { findAsyncTryScopeBeforeLoop, findScope, LoopContinued, ScopeType } from "../utils/scope"; -import { isInAsyncFunction } from "../utils/typescript"; +import { findScope, findTryScopeBeforeLoop, LoopContinued, ScopeType } from "../utils/scope"; export const transformBreakStatement: FunctionVisitor = (breakStatement, context) => { - const tryScope = isInAsyncFunction(breakStatement) ? findAsyncTryScopeBeforeLoop(context) : undefined; + const tryScope = findTryScopeBeforeLoop(context); if (tryScope) { - tryScope.asyncTryHasBreak = true; + tryScope.tryHasBreak = true; return [ lua.createAssignmentStatement( lua.createIdentifier("____hasBroken"), @@ -40,9 +39,9 @@ export const transformContinueStatement: FunctionVisitor = scope.loopContinued = continuedWith; } - const tryScope = isInAsyncFunction(statement) ? findAsyncTryScopeBeforeLoop(context) : undefined; + const tryScope = findTryScopeBeforeLoop(context); if (tryScope) { - tryScope.asyncTryHasContinue = continuedWith; + tryScope.tryHasContinue = continuedWith; return [ lua.createAssignmentStatement( lua.createIdentifier("____hasContinued"), diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index 77067dda8..0ab2d0e61 100644 --- a/src/transformation/visitors/errors.ts +++ b/src/transformation/visitors/errors.ts @@ -79,8 +79,8 @@ const transformAsyncTry: FunctionVisitor = (statement, context) chainCalls.push(lua.createExpressionStatement(promiseAwait, statement)); const hasReturn = tryScope.asyncTryHasReturn ?? catchScope?.asyncTryHasReturn; - const hasBreak = tryScope.asyncTryHasBreak ?? catchScope?.asyncTryHasBreak; - const hasContinue = tryScope.asyncTryHasContinue ?? catchScope?.asyncTryHasContinue; + const hasBreak = tryScope.tryHasBreak ?? catchScope?.tryHasBreak; + const hasContinue = tryScope.tryHasContinue ?? catchScope?.tryHasContinue; // Build result in output order: flag declarations, awaiter, chain calls, post-checks const result: lua.Statement[] = []; @@ -114,7 +114,12 @@ const transformAsyncTry: FunctionVisitor = (statement, context) if (hasBreak) { result.push( - lua.createIfStatement(lua.createIdentifier("____hasBroken"), lua.createBlock([lua.createBreakStatement()])) + lua.createIfStatement( + lua.createIdentifier("____hasBroken", statement), + lua.createBlock([lua.createBreakStatement(statement)], statement), + undefined, + statement + ) ); } @@ -125,21 +130,30 @@ const transformAsyncTry: FunctionVisitor = (statement, context) const continueStatements: lua.Statement[] = []; switch (hasContinue) { case LoopContinued.WithGoto: - continueStatements.push(lua.createGotoStatement(label)); + continueStatements.push(lua.createGotoStatement(label, statement)); break; case LoopContinued.WithContinue: - continueStatements.push(lua.createContinueStatement()); + continueStatements.push(lua.createContinueStatement(statement)); break; case LoopContinued.WithRepeatBreak: continueStatements.push( - lua.createAssignmentStatement(lua.createIdentifier(label), lua.createBooleanLiteral(true)) + lua.createAssignmentStatement( + lua.createIdentifier(label, statement), + lua.createBooleanLiteral(true), + statement + ) ); - continueStatements.push(lua.createBreakStatement()); + continueStatements.push(lua.createBreakStatement(statement)); break; } result.push( - lua.createIfStatement(lua.createIdentifier("____hasContinued"), lua.createBlock(continueStatements)) + lua.createIfStatement( + lua.createIdentifier("____hasContinued", statement), + lua.createBlock(continueStatements, statement), + undefined, + statement + ) ); } @@ -151,7 +165,9 @@ export const transformTryStatement: FunctionVisitor = (statemen return transformAsyncTry(statement, context); } - const [tryBlock, tryScope] = transformScopeBlock(context, statement.tryBlock, ScopeType.Try); + const tsTryBlock = statement.tryBlock; + const tsCatchClause = statement.catchClause; + const [tryBlock, tryScope] = transformScopeBlock(context, tsTryBlock, ScopeType.Try); if ( (context.options.luaTarget === LuaTarget.Lua50 || context.options.luaTarget === LuaTarget.Lua51) && @@ -163,102 +179,193 @@ export const transformTryStatement: FunctionVisitor = (statemen return tryBlock.statements; } - const tryResultIdentifier = lua.createIdentifier("____try"); - const returnValueIdentifier = lua.createIdentifier("____returnValue"); + const trySuccessIdentifier = lua.createIdentifier("____trySuccess", tsTryBlock); + const catchSuccessIdentifier = lua.createIdentifier("____catchSuccess", tsCatchClause); + const hasReturnOrErrorIdentifier = lua.createIdentifier("____hasReturnOrError", tsTryBlock); + const returnValueIdentifier = lua.createIdentifier("____returnValue", tsTryBlock); const result: lua.Statement[] = []; - const returnedIdentifier = lua.createIdentifier("____hasReturned"); - let returnCondition: lua.Expression | undefined; - - const pCall = lua.createIdentifier("pcall"); - const tryCall = lua.createCallExpression(pCall, [lua.createFunctionExpression(tryBlock)]); + // pcall(function() ... end) + const tryPCall = lua.createIdentifier("pcall", tsTryBlock); + const tryCall = lua.createCallExpression(tryPCall, [lua.createFunctionExpression(tryBlock)], tsTryBlock); + // ____trySuccess, ____hasReturnOrError, ____returnValue + const tryReturnIdentifiers = [trySuccessIdentifier, hasReturnOrErrorIdentifier, returnValueIdentifier]; + result.push(lua.createVariableDeclarationStatement(tryReturnIdentifiers, tryCall, tsTryBlock)); - if (statement.catchClause && statement.catchClause.block.statements.length > 0) { - // try with catch - const [catchFunction, catchScope] = transformCatchClause(context, statement.catchClause); - const catchIdentifier = lua.createIdentifier("____catch"); - result.push(lua.createVariableDeclarationStatement(catchIdentifier, catchFunction)); + const hasCatch = tsCatchClause && tsCatchClause.block.statements.length > 0; + let catchScope: Scope | undefined; + if (hasCatch) { + // local ____catchSuccess + result.push(lua.createVariableDeclarationStatement(catchSuccessIdentifier, undefined, tsCatchClause)); - const hasReturn = tryScope.functionReturned ?? catchScope.functionReturned; + const [catchFunction, cScope] = transformCatchClause(context, tsCatchClause); + catchScope = cScope; - const tryReturnIdentifiers = [tryResultIdentifier]; // ____try - if (hasReturn || statement.catchClause.variableDeclaration) { - tryReturnIdentifiers.push(returnedIdentifier); // ____returned - if (hasReturn) { - tryReturnIdentifiers.push(returnValueIdentifier); // ____returnValue - returnCondition = lua.cloneIdentifier(returnedIdentifier); - } - } - result.push(lua.createVariableDeclarationStatement(tryReturnIdentifiers, tryCall)); + const catchIdentifier = lua.createIdentifier("____catch", tsCatchClause); + result.push(lua.createVariableDeclarationStatement(catchIdentifier, catchFunction, tsCatchClause)); + // pcall(____catch, ____hasReturnOrError) + const catchPCall = lua.createIdentifier("pcall", tsCatchClause); const catchCall = lua.createCallExpression( - catchIdentifier, - statement.catchClause.variableDeclaration ? [lua.cloneIdentifier(returnedIdentifier)] : [] + catchPCall, + [catchIdentifier, lua.cloneIdentifier(hasReturnOrErrorIdentifier, tsCatchClause)], + tsTryBlock ); - const catchCallStatement = hasReturn - ? lua.createAssignmentStatement( - [lua.cloneIdentifier(returnedIdentifier), lua.cloneIdentifier(returnValueIdentifier)], - catchCall - ) - : lua.createExpressionStatement(catchCall); - - const notTryCondition = lua.createUnaryExpression(tryResultIdentifier, lua.SyntaxKind.NotOperator); - result.push(lua.createIfStatement(notTryCondition, lua.createBlock([catchCallStatement]))); - } else if (tryScope.functionReturned) { - // try with return, but no catch - // returnedIdentifier = lua.createIdentifier("____returned"); - const returnedVariables = [tryResultIdentifier, returnedIdentifier, returnValueIdentifier]; - result.push(lua.createVariableDeclarationStatement(returnedVariables, tryCall)); - - // change return condition from '____returned' to '____try and ____returned' - returnCondition = lua.createBinaryExpression( - lua.cloneIdentifier(tryResultIdentifier), - returnedIdentifier, - lua.SyntaxKind.AndOperator + + // ____catchSuccess, ____hasReturnOrError, ____returnValue + // = pcall(____catch, ____hasReturnOrError) + const catchAssign = lua.createAssignmentStatement( + [ + lua.cloneIdentifier(catchSuccessIdentifier, tsCatchClause), + lua.cloneIdentifier(hasReturnOrErrorIdentifier, tsCatchClause), + lua.cloneIdentifier(returnValueIdentifier, tsCatchClause), + ], + catchCall, + tsCatchClause + ); + + // if not ____trySuccess then + // ____catchSuccess, ____hasReturnOrError, ____returnValue + // = pcall(____catch, ____hasReturnOrError) + // end + const notTryCondition = lua.createUnaryExpression(trySuccessIdentifier, lua.SyntaxKind.NotOperator, tsTryBlock); + result.push( + lua.createIfStatement(notTryCondition, lua.createBlock([catchAssign], tsCatchClause), undefined, tsTryBlock) ); - } else if (statement.finallyBlock) { - // try without catch, but with finally — need to capture error for re-throw - const errorIdentifier = lua.createIdentifier("____error"); - result.push(lua.createVariableDeclarationStatement([tryResultIdentifier, errorIdentifier], tryCall)); - } else { - // try without return or catch - result.push(lua.createExpressionStatement(tryCall)); } if (statement.finallyBlock && statement.finallyBlock.statements.length > 0) { result.push(...context.transformStatements(statement.finallyBlock)); } - // Re-throw error if try had no catch but had a finally. - // On pcall failure the error is the second return value, which lands in - // ____hasReturned (when functionReturned) or ____error (otherwise). - if (!statement.catchClause && statement.finallyBlock) { - const notTryCondition = lua.createUnaryExpression( - lua.cloneIdentifier(tryResultIdentifier), - lua.SyntaxKind.NotOperator + const trySuccessBlock = lua.createBlock( + [createReturnIfHasReturnOrError(context, statement, hasReturnOrErrorIdentifier, returnValueIdentifier)], + statement + ); + + // error(____hasReturnOrError, 0) + const rethrow = lua.createExpressionStatement( + lua.createCallExpression( + lua.createIdentifier("error"), + [lua.cloneIdentifier(hasReturnOrErrorIdentifier, statement), lua.createNumericLiteral(0)], + statement.catchClause ?? statement.tryBlock + ), + statement.tryBlock + ); + const throwBlock = lua.createBlock([rethrow], statement); + + let ifCatchSuccessStatement: lua.IfStatement | undefined; + if (hasCatch) { + const catchSuccessBlock = lua.createBlock( + [createReturnIfHasReturnOrError(context, statement, hasReturnOrErrorIdentifier, returnValueIdentifier)], + statement ); - const errorIdentifier = tryScope.functionReturned - ? lua.cloneIdentifier(returnedIdentifier) - : lua.createIdentifier("____error"); - const rethrow = lua.createExpressionStatement( - lua.createCallExpression(lua.createIdentifier("error"), [errorIdentifier, lua.createNumericLiteral(0)]) + + ifCatchSuccessStatement = lua.createIfStatement( + lua.cloneIdentifier(catchSuccessIdentifier, statement), + catchSuccessBlock, + throwBlock, + statement ); - result.push(lua.createIfStatement(notTryCondition, lua.createBlock([rethrow]))); } - if (returnCondition && returnedIdentifier) { - const returnValues: lua.Expression[] = []; + let elseBranch: lua.Block | lua.IfStatement | undefined; + if (hasCatch) { + // try {} catch(e) { ... } + // Non-empty catch: check catch success, otherwise re-throw + elseBranch = ifCatchSuccessStatement; + } else if (!tsCatchClause) { + // try {} finally {} + // No catch clause: re-throw uncaught error after finally + elseBranch = throwBlock; + } else { + // try {} catch(e) {} + // Empty catch block: error is intentionally swallowed + } - if (isInMultiReturnFunction(context, statement)) { - returnValues.push(createUnpackCall(context, lua.cloneIdentifier(returnValueIdentifier))); - } else { - returnValues.push(lua.cloneIdentifier(returnValueIdentifier)); + // if ____trySuccess then + // if ____hasReturnOrError then + // return ____returnValue + // end + // elseif ____catchSuccess then + // if ____hasReturnOrError then + // return ____returnValue + // end + // else + // error(____hasReturnOrError, 0) + // end + const ifTrySuccessStatement = lua.createIfStatement( + lua.cloneIdentifier(trySuccessIdentifier, statement), + trySuccessBlock, + elseBranch, + statement + ); + result.push(ifTrySuccessStatement); + + // local ____hasBroken + // local ____hasContinued + const hasBreak = tryScope.tryHasBreak ?? catchScope?.tryHasBreak; + const hasContinue = tryScope.tryHasContinue ?? catchScope?.tryHasContinue; + + if (hasBreak || hasContinue !== undefined) { + const flagDecls: lua.Identifier[] = []; + if (hasBreak) flagDecls.push(lua.createIdentifier("____hasBroken", statement)); + if (hasContinue !== undefined) flagDecls.push(lua.createIdentifier("____hasContinued", statement)); + result.unshift(lua.createVariableDeclarationStatement(flagDecls, undefined, statement)); + } + + // if ____hasBroken then + // break + // end + if (hasBreak) { + result.push( + lua.createIfStatement( + lua.createIdentifier("____hasBroken", statement), + lua.createBlock([lua.createBreakStatement(statement)], statement), + undefined, + statement + ) + ); + } + + // if ____hasContinued then + // goto __continueN (Lua 5.2+) + // continue (Luau) + // __continueN = true; break (Lua 5.0/5.1) + // end + if (hasContinue !== undefined) { + const loopScope = findScope(context, ScopeType.Loop); + const label = `__continue${loopScope?.id ?? ""}`; + + const continueStatements: lua.Statement[] = []; + switch (hasContinue) { + case LoopContinued.WithGoto: + continueStatements.push(lua.createGotoStatement(label, statement)); + break; + case LoopContinued.WithContinue: + continueStatements.push(lua.createContinueStatement(statement)); + break; + case LoopContinued.WithRepeatBreak: + continueStatements.push( + lua.createAssignmentStatement( + lua.createIdentifier(label, statement), + lua.createBooleanLiteral(true), + statement + ) + ); + continueStatements.push(lua.createBreakStatement(statement)); + break; } - const returnStatement = createReturnStatement(context, returnValues, statement); - const ifReturnedStatement = lua.createIfStatement(returnCondition, lua.createBlock([returnStatement])); - result.push(ifReturnedStatement); + result.push( + lua.createIfStatement( + lua.createIdentifier("____hasContinued", statement), + lua.createBlock(continueStatements, statement), + undefined, + statement + ) + ); } return lua.createDoStatement(result, statement); @@ -294,3 +401,26 @@ function transformCatchClause( return [catchFunction, catchScope]; } + +// if ____hasReturnOrError then +// return ____returnValue +// end +function createReturnIfHasReturnOrError( + context: TransformationContext, + statement: ts.TryStatement, + hasReturnOrErrorIdentifier: lua.Identifier, + returnValueIdentifier: lua.Identifier +): lua.IfStatement { + const returnValues: lua.Expression[] = []; + if (isInMultiReturnFunction(context, statement)) { + returnValues.push(createUnpackCall(context, lua.cloneIdentifier(returnValueIdentifier, statement))); + } else { + returnValues.push(lua.cloneIdentifier(returnValueIdentifier, statement)); + } + return lua.createIfStatement( + lua.cloneIdentifier(hasReturnOrErrorIdentifier, statement), + lua.createBlock([createReturnStatement(context, returnValues, statement)], statement), + undefined, + statement + ); +} diff --git a/test/unit/error.spec.ts b/test/unit/error.spec.ts index 8204cc36a..3f3e94487 100644 --- a/test/unit/error.spec.ts +++ b/test/unit/error.spec.ts @@ -7,9 +7,7 @@ test("throwString", () => { `.expectToEqual(new util.ExecutionError("Some Error")); }); -// TODO: Finally does not behave like it should, see #1137 -// eslint-disable-next-line jest/no-disabled-tests -test.skip.each([0, 1, 2])("re-throw (%p)", i => { +test.each([0, 1, 2])("re-throw (%p)", i => { util.testFunction` const i: number = ${i}; function foo() { @@ -140,6 +138,204 @@ test("multi return from catch", () => { testBuilder.expectToMatchJsResult(); }); +test("return from try and catch", () => { + util.testFunction` + function foobar() { + try { + if (false) { + throw "error"; + } + return "try"; + } catch (e) { + return "catch"; + } + return "unreachable"; + } + return foobar(); + `.expectToMatchJsResult(); +}); + +test("return or throw from try and return from catch", () => { + util.testFunction` + function foobar() { + try { + if (true) { + throw "error"; + } + return "try"; + } catch (e) { + return "catch"; + } + return "unreachable"; + } + return foobar(); + `.expectToMatchJsResult(); +}); + +test("return from try catch finally", () => { + util.testFunction` + function foobar() { + let x = ""; + try { + x += "A"; + return x + "try"; + } catch (e) { + x += "B"; + return x + "catch"; + } finally { + x += "C"; + return x + "finally"; + } + x += "D"; + return x + "unreachable"; + } + return foobar(); + `.expectToMatchJsResult(); +}); + +test("try throw, catch throw, finally return", () => { + util.testFunction` + function foobar() { + let x = ""; + try { + x += "A"; + if (true) { + throw "try error"; + } + return x + "try"; + } catch (e) { + x += "B"; + if (true) { + throw e + "catch error"; + } + return x + "catch"; + } finally { + x += "C"; + return x + "finally"; + } + x += "D"; + return x + "unreachable"; + } + return foobar(); + `.expectToMatchJsResult(); +}); + +test("try throw, catch throw, finally throw", () => { + util.testFunction` + function foobar() { + let x = ""; + try { + x += "A"; + if (true) { + throw x + "try error"; + } + return x + "try"; + } catch (e) { + x += "B"; + if (true) { + throw e + x + "catch error"; + } + return x + "catch"; + } finally { + x += "C"; + if (true) { + throw x + "finally error"; + } + } + x += "D"; + return x + "unreachable"; + } + return foobar(); + `.expectToMatchJsResult(true); +}); + +test("return from try->finally no catch", () => { + util.testFunction` + let x = "unevaluated"; + function evaluate(arg: unknown) { + x = "evaluated"; + return arg; + } + function foobar() { + try { + return evaluate("foobar"); + } finally { + return "finally"; + } + } + return foobar() + " " + x; + `.expectToMatchJsResult(); +}); + +test("try throw, no catch, finally no control", () => { + util.testFunction` + let x = ""; + function foo() { + try { + x += "A"; + throw "foo"; + } finally { + x += "B"; + } + } + try { + x += "C"; + return foo() + " " + x; + } catch { + x += "D"; + return "caught " + x; + } + `.expectToMatchJsResult(true); +}); + +test("try throw, catch no control, finally no control", () => { + util.testFunction` + let x = ""; + function foo() { + try { + x += "A"; + throw "foo"; + } catch (e) { + x += "B"; + } finally { + x += "C"; + } + } + try { + x += "D"; + const result = foo() ?? "void"; + return result + " " + x; + } catch(e) { + x += "E"; + return "caught " + e + " " + x; + } + `.expectToMatchJsResult(true); +}); + +test("try throw, catch throw, finally no control", () => { + util.testFunction` + let x = ""; + function foo() { + try { + x += "A"; + throw "foo"; + } catch (e) { + x += "B"; + throw e + " catch"; + } finally { + x += "C"; + } + } + try { + x += "D"; + return foo() + " " + x; + } catch { + x += "E"; + return "caught " + x; + } + `.expectToMatchJsResult(true); +}); + test("return from nested try", () => { util.testFunction` function foobar() { @@ -493,6 +689,130 @@ test("try/finally rethrow with non-string error", () => { `.expectToMatchJsResult(); }); +test("break inside try in loop", () => { + util.testFunction` + const result: number[] = []; + for (let i = 0; i < 5; i++) { + try { + if (i === 3) break; + result.push(i); + } catch {} + } + return result; + `.expectToMatchJsResult(); +}); + +test("continue inside try in loop", () => { + util.testFunction` + const result: number[] = []; + for (let i = 0; i < 5; i++) { + try { + if (i === 2) continue; + result.push(i); + } catch {} + } + return result; + `.expectToMatchJsResult(); +}); + +test("break inside catch in loop", () => { + util.testFunction` + const result: number[] = []; + for (let i = 0; i < 5; i++) { + try { + throw i; + } catch (e: any) { + if (e === 3) break; + result.push(e); + } + } + return result; + `.expectToMatchJsResult(); +}); + +test("continue inside catch in loop", () => { + util.testFunction` + const result: number[] = []; + for (let i = 0; i < 5; i++) { + try { + throw i; + } catch (e: any) { + if (e === 2) continue; + result.push(e); + } + } + return result; + `.expectToMatchJsResult(); +}); + +test("break inside try with finally in loop", () => { + util.testFunction` + const result: number[] = []; + let finallyCalls = 0; + for (let i = 0; i < 5; i++) { + try { + if (i === 3) break; + result.push(i); + } finally { + finallyCalls++; + } + } + return { result, finallyCalls }; + `.expectToMatchJsResult(); +}); + +test("continue inside try with finally in loop", () => { + util.testFunction` + const result: number[] = []; + let finallyCalls = 0; + for (let i = 0; i < 5; i++) { + try { + if (i === 2) continue; + result.push(i); + } finally { + finallyCalls++; + } + } + return { result, finallyCalls }; + `.expectToMatchJsResult(); +}); + +test("break inside catch with finally in loop", () => { + util.testFunction` + const result: number[] = []; + let finallyCalls = 0; + for (let i = 0; i < 5; i++) { + try { + throw i; + } catch (e: any) { + if (e === 3) break; + result.push(e); + } finally { + finallyCalls++; + } + } + return { result, finallyCalls }; + `.expectToMatchJsResult(); +}); + +test("continue inside catch with finally in loop", () => { + util.testFunction` + const result: number[] = []; + let finallyCalls = 0; + for (let i = 0; i < 5; i++) { + try { + throw i; + } catch (e: any) { + if (e === 2) continue; + result.push(e); + } finally { + finallyCalls++; + } + } + return { result, finallyCalls }; + `.expectToMatchJsResult(); +}); + util.testEachVersion( "error stacktrace omits constructor and __TS_New", () => util.testFunction` diff --git a/test/util.ts b/test/util.ts index 1e191fb28..26c1ff8e1 100644 --- a/test/util.ts +++ b/test/util.ts @@ -557,8 +557,11 @@ end)());`; result = vm.runInContext(this.getJsCodeWithWrapper(), globalContext); } catch (error) { const hasMessage = (error: any): error is { message: string } => error.message !== undefined; - assert(hasMessage(error)); - return new ExecutionError(error.message); + if (hasMessage(error)) { + return new ExecutionError(error.message); + } else { + return new ExecutionError(String(error)); + } } function removeUndefinedFields(obj: any): any {