return 以外にフレームが消える経路があります。例外です。throw されると、その行より後ろは実行されず、フレームは 1 つずつ消えながら、catch のある呼び出し元まで戻ります。ただし、途中にある finally は実行されます。

throw されると、フレームが 1 つずつ消えて catch のある呼び出し元まで戻る

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 プロパティには投げた時点の履歴が残りますが、いまのスタックは巻き戻し済みです。