try..finally
You’re probably familiar with how the try..catch
block works. But have you ever stopped to consider the finally
clause that can be paired with it? In fact, were you aware that try
only requires either catch
or finally
, though both can be present if needed.
The code in the finally
clause always runs (no matter what), and it always runs right after the try
(and catch
if present) finish, before any other code runs. In one sense, you can kind of think of the code in a finally
clause as being in a callback function that will always be called regardless of how the rest of the block behaves.
So what happens if there’s a return
statement inside a try
clause? It obviously will return a value, right? But does the calling code that receives that value run before or after the finally
?
function foo() {
try {
return 42;
}
finally {
console.log( "Hello" );
}
console.log( "never runs" );
}
console.log( foo() );
// Hello
// 42
The return 42
runs right away, which sets up the completion value from the foo()
call. This action completes the try
clause and the finally
clause immediately runs next. Only then is the foo()
function complete, so that its completion value is returned back for the console.log(..)
statement to use.
The exact same behavior is true of a throw
inside try
:
function foo() {
try {
throw 42;
}
finally {
console.log( "Hello" );
}
console.log( "never runs" );
}
console.log( foo() );
// Hello
// Uncaught Exception: 42
Now, if an exception is thrown (accidentally or intentionally) inside a finally
clause, it will override as the primary completion of that function. If a previous return
in the try
block had set a completion value for the function, that value will be abandoned.
function foo() {
try {
return 42;
}
finally {
throw "Oops!";
}
console.log( "never runs" );
}
console.log( foo() );
// Uncaught Exception: Oops!
It shouldn’t be surprising that other nonlinear control statements like continue
and break
exhibit similar behavior to return
and throw
:
for (var i=0; i<10; i++) {
try {
continue;
}
finally {
console.log( i );
}
}
// 0 1 2 3 4 5 6 7 8 9
The console.log(i)
statement runs at the end of the loop iteration, which is caused by the continue
statement. However, it still runs before the i++
iteration update statement, which is why the values printed are 0..9
instead of 1..10
.
Note: ES6 adds a yield
statement, in generators (see the Async & Performance title of this series) which in some ways can be seen as an intermediate return
statement. However, unlike a return
, a yield
isn’t complete until the generator is resumed, which means a try { .. yield .. }
has not completed. So an attached finally
clause will not run right after the yield
like it does with return
.
A return
inside a finally
has the special ability to override a previous return
from the try
or catch
clause, but only if return
is explicitly called:
function foo() {
try {
return 42;
}
finally {
// no `return ..` here, so no override
}
}
function bar() {
try {
return 42;
}
finally {
// override previous `return 42`
return;
}
}
function baz() {
try {
return 42;
}
finally {
// override previous `return 42`
return "Hello";
}
}
foo(); // 42
bar(); // undefined
baz(); // "Hello"
Normally, the omission of return
in a function is the same as return;
or even return undefined;
, but inside a finally
block the omission of return
does not act like an overriding return undefined
; it just lets the previous return
stand.
In fact, we can really up the craziness if we combine finally
with labeled break
(discussed earlier in the chapter):
function foo() {
bar: {
try {
return 42;
}
finally {
// break out of `bar` labeled block
break bar;
}
}
console.log( "Crazy" );
return "Hello";
}
console.log( foo() );
// Crazy
// Hello
But… don’t do this. Seriously. Using a finally
+ labeled break
to effectively cancel a return
is doing your best to create the most confusing code possible. I’d wager no amount of comments will redeem this code.