JS: try { return } VS finally { return }

公開:2021/02/05
更新:2021/02/05
2 min読了の目安(約2400字TECH技術記事

JavaScriptでは「try内のreturn」と「finally内のreturn」が一連の実行パス上に存在するとき、「finally内のreturn」が優先されるらしい。

「まあそうなるやろな」というのはわかりつつ、仕様としてどうなっているのかが気になってECMAScriptの仕様書を読んだので解説するぞ!

https://twitter.com/hanak1a_/status/1357636387579645957

tryの仕様

tryステートメントの仕様はここに書いてある

https://262.ecma-international.org/#sec-try-statement

Syntaxの項を見ると、tryステートメントメントにはざっくりいうと以下のような構文が定義されているようだ (最近catchのパラメータが任意になりましたね)

try { } catch (identifier) { }
try { } finally (identifier) { }
try { } catch (e) finally (identifier) { }

まあこれは皆さんご存知という感じですね。
では実際の動作を定義しているSemanticsの項を読んでみましょう。

https://262.ecma-international.org/#sec-try-statement-runtime-semantics-evaluation

この内、try TryStatement : try Block Catch Finallyでfinallyの挙動が定義されています。

雑に翻訳すると

  1. tryブロックを実行し、その結果をBとする
  2. B.[[Type]]throwなら、catch節にB.[[Value]]を渡して実行し、その結果をCとする
  3. B.[[Type]]がthrow以外ならB(tryの結果) = C(catchの結果)とする
  4. finallyを実行し、その結果をFとする
  5. F.[[Type]]がnormalならF(finallyの結果) = C(Catchの結果)とする
  6. Completion(UpdateEmpty(F, undefined))を返す

ということらしいっす。

つまり、tryとfinallyの両方にreturnがある場合、

  • 4.でfinally内のreturnが評価され、
  • 5.F.[[Type]]がnormalではない(returnになる)ため、F.[[Value]]が返される

ということだ!

UpdateEmptyとかCompletionとかよくわからない言葉が出てきたので、次はこれにそれらについて調べてみるぞ!

UpdateEmpty(completionRecord, value)

パッといえば、UpdateEmptyは「Completionを生成する」なにかだ。
正確には「completionRecordが正しい状態かをアサーションして、Completionを生成する」役割らしい。

それ以上の定義はないのでこれでUpdateEmptyの解説は終わり!!!!!!!!!!

https://262.ecma-international.org/#sec-updateempty

Completion

ということで、どうやらCompletionというのがここいらの話の本質っぽい。

https://262.ecma-international.org/#sec-completion-record-specification-type

詳細なことは理解しきれてないけど、察するに、Completionとは「JavaScriptエンジン内部のフロー制御用の型」で、「ブロックの終了状態を表す型」のようだ(もしかしたら関数の終了状態も表してるかもしれない)

具体的には以下のような構造体っぽい

type Completion = {
  [[Type]]: 'normal' | 'break' | 'continue' | 'return' | 'throw'
  [[Value]]: any
  [[Target]]: string | empty
}

Completion.[[Type]]がそのフローがどのように終了したかを表している。

「finally内のreturn」 で終了した場合、.[[Type]]はおそらくreturnになる。 で、プログラマーが明示的に処理を終了させなかった場合は多分normalになるんだろうな(知らんけど)

なので、finallyを実行したあとのフローの4.6.の仕様で、finally内のreturnが優先される。

4. finallyを実行し、その結果をFとする
  - `F.[[Type]]`は return になる
5. `F.[[Type]]`がnormalなら`F(finallyの結果)` = `C(Catchの結果)`とする
  - `F.[[Type]]`は normal ではない(return)
  - → Fはfinallyの結果のままになる
6. Completion(UpdateEmpty(F, undefined))を返す
  - → Finallyの完了状態が返される

そういうことらしいです。