Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/transformation/visitors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,19 @@ export const transformTryStatement: FunctionVisitor<ts.TryStatement> = (statemen
result.push(...context.transformStatements(statement.finallyBlock));
}

// Re-throw error if try had no catch but had a finally
// 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 errorIdentifier = lua.createIdentifier("____error");
const errorIdentifier = tryScope.functionReturned
? lua.cloneIdentifier(returnedIdentifier)
: lua.createIdentifier("____error");
const rethrow = lua.createExpressionStatement(
lua.createCallExpression(lua.createIdentifier("error"), [
lua.cloneIdentifier(errorIdentifier),
lua.createNumericLiteral(0),
])
lua.createCallExpression(lua.createIdentifier("error"), [errorIdentifier, lua.createNumericLiteral(0)])
);
result.push(lua.createIfStatement(notTryCondition, lua.createBlock([rethrow])));
}
Expand Down
55 changes: 55 additions & 0 deletions test/unit/error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,61 @@ test("sourceMapTraceback maps anonymous function locations in .lua files (#1665)
expect(result).toContain("<main.ts:2>");
});

test("try/finally rethrow preserves error value", () => {
util.testFunction`
function foo() {
try {
throw "oops";
} finally {
}
}
try { foo(); return "no error"; } catch(e) { return e; }
`.expectToMatchJsResult();
});

test("try/finally with return and throw paths", () => {
util.testFunction`
function foo(shouldReturn: boolean) {
try {
if (shouldReturn) return "returned";
throw "thrown";
} finally {
}
}
const results: any[] = [];
results.push(foo(true));
try { foo(false); } catch(e) { results.push(e); }
return results;
`.expectToMatchJsResult();
});

test("try/finally runs finally side effect before rethrow", () => {
util.testFunction`
let sideEffect = false;
function foo() {
try {
throw "err";
} finally {
sideEffect = true;
}
}
try { foo(); } catch(e) {}
return sideEffect;
`.expectToMatchJsResult();
});

test("try/finally rethrow with non-string error", () => {
util.testFunction`
function foo() {
try {
throw 42;
} finally {
}
}
try { foo(); return "no error"; } catch(e) { return e; }
`.expectToMatchJsResult();
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have one more test for return + throw + non-empty finally body

function foo(shouldReturn: boolean) {
    try {
        if (shouldReturn) return "ok";
        throw "err";
    } finally {
        sideEffect = true; // must still run on both paths
    }
}

util.testEachVersion(
"error stacktrace omits constructor and __TS_New",
() => util.testFunction`
Expand Down
Loading