TryCatch のスコープではまること

V8 における文字列の評価処理は、だいたい次のような流れをとります。

  const char* lpszScript = ...;
  
  HandleScope handle_scope;
  TryCatch try_catch;
  Local<Script> script = Script::Compile(String::New(lpszScript));
  if (script.IsEmpty()) {
    ReportException(try_catch.Exception());
  } else {
    Local<Value> result = script->Run();
    if (result.IsEmpty()) {
      ReportException(try_catch.Exception());
    } else {
      Print(result);
    }
  }

コマンド入力だったらこんなのでよいかもしれませんが、スクリプトをファイルから読み込ませるような関数を書いている場合、その場で ReportException を呼ぶより、外に例外を投げる方が設計上好ましいように思われます。とにかくそこからは例外で脱出して、インタプリタのトップレベルによって一度だけ例外を報告させることができるからです。
V8 でコールバックが例外を投げる場合は、ThrowException() という関数を使ってそのまま return するように求められていますが、上のコードをそのまま流用したりして、直感的にはだいたいこんな感じで書いてしまいがちです。

  HandleScope handle_scope;
  TryCatch try_catch;
  Local<Script> script = Script::Compile(String::New(lpszScript));
  if (script.IsEmpty()) {
    return ThrowException(try_catch.Exception());
  }
  Local<Value> result = script->Run();
  if (result.IsEmpty()) {
    return ThrowException(try_catch.Exception());
  }

実はこれは正しく動きません。


なぜかというと、ThrowException() を呼んでいるところがまだ try_catch のスコープ内なので、このスコープにある try_catch が投げた例外を捕まえてしまうからです。本来の意図としてはここより外の try_catch に例外を投げたいのですが、スコープに囚われてしまうと。
なので、下記の v8-users のスレッドにもありますが、ここは正しくは次のように書くべきとのことです。
http://groups.google.com/group/v8-users/browse_thread/thread/ea8e610db7364276

  HandleScope handle_scope;
  Handle<Value> exception;
  {
    TryCatch try_catch;
    Local<Script> script = Script::Compile(String::New(lpszScript));
    if (script.IsEmpty()) {
      exception = Local<Value>::New(try_catch.Exception());
    } else {
      Local<Value> result = script->Run();
      if (result.IsEmpty()) {
        exception = Local<Value>::New(try_catch.Exception());
      }
    }
  } // ここで try_catch のスコープを抜けるので、外に例外を投げられるようになる
  if (!exception.IsEmpty()) {
    return ThrowException(exception);
  }

ちょっとまどろっこしい書き方ですが、要するに try_catch のスコープを外れてから ThrowException() する必要があるということです。
api.cc あたりのコードを見るとわかりますが、TryCatch は内部的には例外ハンドラの linked list を管理していて、コンストラクタで自分を先頭に登録、デストラクタで削除というようなことをやっています。で、例外はそのリストの先頭に送られるという仕掛け。なのでデストラクトされないと次のレベルの例外ハンドラが現れないわけですね。
また、例外を新しいハンドルに入れているのは、try_catch が返すハンドルは try_catch のデストラクタで解放されてしまうため、とのことです。(test/cctest/test-api.cc, ThrowInJS() を参照のこと。)