return 以外にフレームが消える経路があります。例外です。throw されると、その行より後ろは実行されず、フレームは 1 つずつ消えながら、catch のある呼び出し元まで戻ります。ただし、途中にある finally は実行されます。
f(2) が f(1) を、f(1) が f(0) を呼び、f(0) が throw する例で確かめます。各 f には、呼び出しの後ろに console.log('after call', n) を置いてあります。
// unwind.js
function f(n) {
try {
console.log('enter', n);
if (n === 0) throw new Error('boom');
f(n - 1);
console.log('after call', n);
} finally {
console.log('finally', n);
}
}
try {
f(2);
} catch (e) {
console.log('caught', e.message);
}
after call は一度も出ていません。例外が通り抜けた関数では呼び出しの後ろの行が飛ばされ、finally だけが内側から順に実行されています。
catch の中では、例外を投げた側の関数はもうスタックにいません。
// thrower.js
function names() {
return new Error().stack.split('\n').slice(2)
.map((line) => line.trim().split(' ')[1]).join(' <- ');
}
function thrower() { throw new Error('boom'); }
try {
thrower();
} catch (e) {
console.log('catch の中:', names());
}
thrower は消えていて、一番上は catch を書いたスクリプト本体です。エラーオブジェクトの stack プロパティには投げた時点の履歴が残りますが、いまのスタックは巻き戻し済みです。