8.6. 例外

asyncio スクリプト内での例外は、通常の Python とほとんど同じように振る舞います -- 何かがそれを捕捉するまで呼び出しチェーンを上へと伝播していきます。ほとんど と言うのは、タスクは並行して実行されているため、「上」へ向かう経路はタスクを作成した経路とは異なるからです。このページでは、よくある各形態において例外がどこへ向かうのかを取り上げます。

8.6.1. 1つのコルーチン内で

コルーチン内の try/except は、それが await する何かが送出した例外を、通常どおりに捕捉します:

async def fetch_with_retry(url):
    for attempt in range(3):
        try:
            return await fetch(url)
        except OSError as e:
            last_error = e
    raise last_error

ここに asyncio 固有のものは何もありません -- except 節は、まるで通常の関数呼び出しであったかのように、fetch 内で送出された例外を見ます。

8.6.2. アプリケーションが await しているタスク内で

Task として実行されているコルーチンが例外を送出すると、その例外はタスクに保存されます。次に何かがそのタスクを await したとき、例外はその await の箇所で再送出されます:

task = asyncio.create_task(may_fail())
try:
    result = await task
except OSError:
    log("may_fail failed")

asyncio.gather() にも同じことが当てはまります。デフォルトの動作 -- 1つの子が例外を送出し、他の子はキャンセルされ、例外が gather の外へ伝播する -- は、この仕組みに由来します。

8.6.3. 誰も await しないタスク内で

誰も await しないタスクは、注意を要するケースです。例外は依然として発生します。ループはそのタスクが未処理の例外で終了したことに気づきます。しかし、それを表面化させる await がありません。デフォルトの動作は sys.stderr を通してトレースバックを出力し、実行を続けることです -- これは無人の診断用途には問題ありませんが、それを知りたいと思っていたアプリケーションには不向きです。

正しい対処法は通常、そのタスクを await する ことです。直接的にハンドルを覚えておいてシャットダウン時に await するか、あるいは gather()wait_for() を介して暗黙的に行います。タイムアウトとキャンセル ページの「アプリケーションのシャットダウン」パターンは、典型的なスクリプトが生成する長寿命のバックグラウンドタスクについて、このケースを捕捉します。

8.6.4. カスタム例外ハンドラ

トレースバックを出して静かに続行するだけでは不十分な場合、ループはフック -- Loop.set_exception_handler -- を公開しており、アプリケーションはこれをオーバーライドして別の処理を行えます:

def handler(loop, context):
    print("asyncio:", context.get("message"))
    if "exception" in context:
        sys.print_exception(context["exception"])

loop = asyncio.get_event_loop()
loop.set_exception_handler(handler)

context 引数は 'message''exception''future' というキーを持つ辞書です。特定の警告スタイルのイベントでは例外が欠けていることがあり、この例で .get() を使っているのはそのためです。

典型的な用途は、失敗をフラッシュに記録する、エラーLEDを点滅させる、あるいはウォッチドッグによる再起動へエスカレートするなどです。ループ制御 ページでは、ループフックの全体像を取り上げています。

8.6.5. KeyboardInterrupt

スクリプトが外部から停止されると -- 通常はIDEが停止を要求することで -- その要求は KeyboardInterrupt としてスクリプト内に届きます。asyncio.run() の内部では、これは他の未処理の例外と同じように伝播します。main がキャンセルされ、ループが追跡しているすべてのタスクもキャンセルされ、KeyboardInterruptasyncio.run() の外へ再送出されます。finally 節は抜ける途中で実行されるため、キャンセルのページと同じクリーンアップパターンがこれを処理します。