Skip to content

Commit e783586

Browse files
authored
try/catch improvements (roblox-ts#2726)
Fixes roblox-ts#2647 Fixes roblox-ts#2725 - Removes unused `traceback` variable - Uses `pcall` for simplicity - Rethrows error after running `finally` if `catch` block doesn't exist - If `catch` errors, it will run `finally` and then rethrow the error - Added new tests
1 parent 73b2ebb commit e783586

File tree

2 files changed

+224
-12
lines changed

2 files changed

+224
-12
lines changed

include/RuntimeLib.lua

+39-10
Original file line numberDiff line numberDiff line change
@@ -184,27 +184,56 @@ TS.TRY_RETURN = 1
184184
TS.TRY_BREAK = 2
185185
TS.TRY_CONTINUE = 3
186186

187-
function TS.try(func, catch, finally)
188-
local err, traceback
189-
local success, exitType, returns = xpcall(
190-
func,
191-
function(errInner)
192-
err = errInner
193-
traceback = debug.traceback()
187+
function TS.try(try, catch, finally)
188+
-- execute try
189+
local trySuccess, exitTypeOrTryError, returns = pcall(try)
190+
local exitType, tryError
191+
if trySuccess then
192+
exitType = exitTypeOrTryError
193+
else
194+
tryError = exitTypeOrTryError
195+
end
196+
197+
local catchSuccess = true
198+
local catchError
199+
200+
-- if try block failed, and catch block exists, execute catch
201+
if not trySuccess and catch then
202+
local newExitTypeOrCatchError, newReturns
203+
catchSuccess, newExitTypeOrCatchError, newReturns = pcall(catch, tryError)
204+
local newExitType
205+
if catchSuccess then
206+
newExitType = newExitTypeOrCatchError
207+
else
208+
catchError = newExitTypeOrCatchError
194209
end
195-
)
196-
if not success and catch then
197-
local newExitType, newReturns = catch(err, traceback)
210+
198211
if newExitType then
199212
exitType, returns = newExitType, newReturns
200213
end
201214
end
215+
216+
-- execute finally
202217
if finally then
203218
local newExitType, newReturns = finally()
204219
if newExitType then
205220
exitType, returns = newExitType, newReturns
206221
end
207222
end
223+
224+
-- if exit type is a control flow, do not rethrow errors
225+
if exitType ~= TS.TRY_RETURN and exitType ~= TS.TRY_BREAK and exitType ~= TS.TRY_CONTINUE then
226+
-- if catch block threw an error, rethrow it
227+
if not catchSuccess then
228+
error(catchError, 2)
229+
end
230+
231+
-- if try block threw an error and there was no catch block, rethrow it
232+
if not trySuccess and not catch then
233+
error(tryError, 2)
234+
end
235+
end
236+
208237
return exitType, returns
209238
end
210239

tests/src/tests/try.spec.ts

+185-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export = () => {
1818
return exception;
1919
}
2020
expect(foo()).to.be.a("string");
21+
expect(tostring(foo()).find("bar")[0]).to.be.ok();
2122
});
2223

2324
it("should support try/catch with throwing objects", () => {
@@ -33,13 +34,35 @@ export = () => {
3334
expect(foo()).to.be.a("table");
3435
});
3536

36-
it("should support try/catch with return", () => {
37+
it("should support try/catch with return in try block", () => {
3738
function foo(): unknown {
3839
try {
3940
return "foo";
4041
} catch (e) {}
4142
}
42-
expect(foo()).to.be.a("string");
43+
expect(foo()).to.equal("foo");
44+
});
45+
46+
it("should support try/catch with return in catch block", () => {
47+
function foo(): unknown {
48+
try {
49+
throw undefined;
50+
} catch {
51+
return "foo";
52+
}
53+
}
54+
expect(foo()).to.equal("foo");
55+
});
56+
57+
it("should support try/catch with return in finally block", () => {
58+
function foo(): unknown {
59+
try {
60+
return 1;
61+
} finally {
62+
return 2;
63+
}
64+
}
65+
expect(foo()).to.equal(2);
4366
});
4467

4568
it("should support try/catch with break", () => {
@@ -181,4 +204,164 @@ export = () => {
181204

182205
expect(foo()).to.equal(2);
183206
});
207+
208+
it("should rethrow the error if there's no catch block", () => {
209+
function foo() {
210+
try {
211+
throw "bar";
212+
} finally {
213+
}
214+
}
215+
216+
expect(() => foo()).to.throw("bar");
217+
});
218+
219+
it("should run try -> catch -> finally in order", () => {
220+
const array = new Array<number>();
221+
try {
222+
let condition = true;
223+
try {
224+
array.push(1);
225+
if (condition) throw "error";
226+
array.push(999);
227+
} catch {
228+
array.push(2);
229+
} finally {
230+
array.push(3);
231+
}
232+
} catch {}
233+
234+
expect(array[0]).to.equal(1);
235+
expect(array[1]).to.equal(2);
236+
expect(array[2]).to.equal(3);
237+
});
238+
239+
it("should run try -> finally in order", () => {
240+
const array = new Array<number>();
241+
try {
242+
let condition = true;
243+
try {
244+
array.push(1);
245+
if (condition) throw "error";
246+
array.push(999);
247+
} finally {
248+
array.push(2);
249+
}
250+
} catch {}
251+
252+
expect(array[0]).to.equal(1);
253+
expect(array[1]).to.equal(2);
254+
});
255+
256+
it("should run finally even if catch throws", () => {
257+
let ranFinally = false;
258+
try {
259+
try {
260+
throw "try error";
261+
} catch {
262+
throw "catch error";
263+
} finally {
264+
ranFinally = true;
265+
}
266+
} catch {}
267+
268+
expect(ranFinally).to.equal(true);
269+
});
270+
271+
it("should throw if finally throws", () => {
272+
function foo() {
273+
try {
274+
} finally {
275+
throw "bar";
276+
}
277+
}
278+
279+
expect(() => foo()).to.throw("bar");
280+
});
281+
282+
it("should discard errors if finally has a control flow statement", () => {
283+
function tryErrorReturn() {
284+
try {
285+
throw "try error";
286+
} finally {
287+
return true;
288+
}
289+
return false;
290+
}
291+
292+
expect(tryErrorReturn()).to.equal(true);
293+
294+
function catchErrorReturn() {
295+
try {
296+
throw "try error";
297+
} catch {
298+
throw "catch error";
299+
} finally {
300+
return true;
301+
}
302+
return false;
303+
}
304+
305+
expect(catchErrorReturn()).to.equal(true);
306+
307+
function tryErrorBreak() {
308+
for (let i = 0; i < 10; i++) {
309+
try {
310+
throw "try error";
311+
} finally {
312+
break;
313+
}
314+
return false;
315+
}
316+
return true;
317+
}
318+
319+
expect(tryErrorBreak()).to.equal(true);
320+
321+
function catchErrorBreak() {
322+
for (let i = 0; i < 1; i++) {
323+
try {
324+
throw "try error";
325+
} catch {
326+
throw "catch error";
327+
} finally {
328+
break;
329+
}
330+
return false;
331+
}
332+
return true;
333+
}
334+
335+
expect(catchErrorBreak()).to.equal(true);
336+
337+
function tryErrorContinue() {
338+
for (let i = 0; i < 1; i++) {
339+
try {
340+
throw "try error";
341+
} finally {
342+
continue;
343+
}
344+
return false;
345+
}
346+
return true;
347+
}
348+
349+
expect(tryErrorContinue()).to.equal(true);
350+
351+
function catchErrorContinue() {
352+
for (let i = 0; i < 1; i++) {
353+
try {
354+
throw "try error";
355+
} catch {
356+
throw "catch error";
357+
} finally {
358+
continue;
359+
}
360+
return false;
361+
}
362+
return true;
363+
}
364+
365+
expect(catchErrorContinue()).to.equal(true);
366+
});
184367
};

0 commit comments

Comments
 (0)