2026年5月29日

2026年5月29日

WordPressプラグインでtry-catch-finallyを使う堅牢なエラー処理

WordPressプラグインで try-catch-finally を使うときの基本は、「例外は外部ライブラリや PHP 標準機能との境界で受け止めて WP_Error に変換し、後片付けは finally に書く」ことです。WordPress 本体の関数の多くは例外を投げず WP_Error や false を返すため、try-catch で囲めば安全になるわけではありません。この記事では、2つのエラーの流れを整理したうえで、変換・後片付け・ログの書き方と、finally が実行されない落とし穴を解説します。

利用者やブラウザーへのエラーの返し方(wp_die() や wp_send_json_error() の使い分け)は、別記事で扱っています。こちらはプラグイン内部の処理の組み立てに絞った内容です。

WordPress にある2つのエラーの流れ

プラグインのコードには、性質の違う2種類のエラーが流れてきます。

  1. 戻り値で知らせるエラー:wp_remote_get()、wp_insert_post()(第2引数 true のとき)、wp_update_post() などは失敗時に WP_Error を返します。$wpdb のクエリは失敗すると false を返し、詳細は $wpdb->last_error に入ります。これらは try-catch では捕まえられません。
  2. 例外として飛んでくるエラー:PHP の型エラー(TypeError)、JSON_THROW_ON_ERROR を付けた json_decode() の JsonException、DateTimeImmutable に不正な日付を渡したときの例外、Composer で入れた HTTP クライアントや SDK が投げる例外などです。

この2つを混ぜたまま書くと、「try で囲んだのに失敗に気づかない」「例外が素通りして重大なエラー画面になる」といった事故が起きます。方針として、プラグイン内部の関数は WP_Error を返す形にそろえ、例外は発生源に近い場所で受け止めて変換する、と決めておくと読みやすくなります。

try-catch-finally の基本とPHPのバージョン差

<?php
try {
    // 失敗するかもしれない処理
} catch ( JsonException $e ) {
    // 特定の例外だけを処理する
} catch ( Throwable $e ) {
    // それ以外のすべての Error と Exception
} finally {
    // 成功しても失敗しても実行したい後片付け
}

PHP 7 以降、Exception と Error はどちらも Throwable インターフェースを実装しています。catch ( Exception $e ) だけでは TypeError などの Error 系は捕まえられない点に注意してください。

使える書き方は PHP のバージョンで変わります。

  • catch ( A | B $e ) のように複数の型をまとめる書き方は PHP 7.1 以降
  • JSON_THROW_ON_ERROR は PHP 7.3 以降
  • 変数を書かない catch ( Exception ) は PHP 8.0 以降

配布するプラグインでは、プラグインヘッダーの Requires PHP に書いたバージョンで動く構文だけを使います。WordPress 本体が求める最低 PHP バージョンも引き上げが続いているため(2026年10月現在)、対象にする PHP の範囲を決めてから書き始めると迷いません。

境界で WP_Error に変換する

外部 API の JSON を受け取って処理する例です。HTTP 通信は WP_Error で、JSON の解析は例外で失敗が届くので、両方をここで受け止めて WP_Error にそろえます。

<?php
function myplugin_fetch_remote_settings( $url ) {
    $response = wp_remote_get( $url, array( 'timeout' => 10 ) );

    if ( is_wp_error( $response ) ) {
        return $response; // 通信エラーはそのまま返す
    }

    $code = wp_remote_retrieve_response_code( $response );
    if ( 200 !== $code ) {
        return new WP_Error( 'myplugin_http_status', sprintf( 'Unexpected HTTP status: %d', $code ) );
    }

    try {
        $data = json_decode( wp_remote_retrieve_body( $response ), true, 512, JSON_THROW_ON_ERROR );
    } catch ( JsonException $e ) {
        return new WP_Error( 'myplugin_invalid_json', 'Invalid JSON: ' . $e->getMessage() );
    }

    if ( ! is_array( $data ) || ! isset( $data['version'] ) ) {
        return new WP_Error( 'myplugin_unexpected_format', 'Unexpected response format.' );
    }

    return $data;
}

// 呼び出し側は WP_Error だけを気にすればよい
$settings = myplugin_fetch_remote_settings( 'https://api.example.com/settings' );
if ( is_wp_error( $settings ) ) {
    error_log( 'myplugin: ' . $settings->get_error_code() . ' ' . $settings->get_error_message() );
    return;
}

catch の中で何もせず処理を続ける「握りつぶし」は避けます。失敗したことが記録に残らず、原因の調査ができなくなるからです。最低でも WP_Error に変換して呼び出し側に知らせるか、ログに残します。

finally で後片付けをする

finally の役割は、成功・失敗・途中の return にかかわらず、取得したものを元に戻すことです。プラグインでよく使う場面を3つ紹介します。

1. 二重実行防止のロックを外す

重い取り込み処理を WP-Cron で動かすとき、前回の処理が終わる前に次が始まらないよう簡易ロックをかけることがあります。例外で処理が中断してもロックが残らないよう、解除は finally に書きます。

<?php
function myplugin_run_import() {
    // add_option() は同じ名前のオプションが既にあると false を返す
    if ( ! add_option( 'myplugin_import_lock', time(), '', false ) ) {
        return new WP_Error( 'myplugin_locked', 'Import is already running.' );
    }

    try {
        return myplugin_import_items(); // WP_Error か件数を返す処理
    } catch ( Throwable $e ) {
        error_log( 'myplugin import failed: ' . get_class( $e ) . ': ' . $e->getMessage() );
        return new WP_Error( 'myplugin_import_failed', 'Import failed.' );
    } finally {
        delete_option( 'myplugin_import_lock' );
    }
}

try の中で return しても finally は実行されるので、ロックは解除されます。処理が途中で強制終了した場合に備え、ロックに保存した時刻が古ければ解除する仕組みも用意しておくと、ロックが残り続ける事態を避けられます。

2. 一時的に変えた設定を戻す

PHP の警告(Warning)は例外ではないため、そのままでは catch できません。特定の処理の間だけ警告を例外に変換し、終わったら元に戻す、という使い方ができます。

<?php
function myplugin_read_file( $path ) {
    set_error_handler( static function ( $severity, $message, $file, $line ) {
        if ( ! ( error_reporting() & $severity ) ) {
            return false; // 抑制されているエラーは通常の処理に任せる
        }
        throw new ErrorException( $message, 0, $severity, $file, $line );
    } );

    try {
        return file_get_contents( $path );
    } catch ( ErrorException $e ) {
        return new WP_Error( 'myplugin_read_failed', $e->getMessage() );
    } finally {
        restore_error_handler(); // 元に戻さないと、ほかのプラグインの動作まで変わる
    }
}

restore_error_handler() を finally に置かないと、例外が発生した経路でエラーハンドラーが差し替わったままになり、サイト全体に影響します。

3. データベースのトランザクションを閉じる

複数のテーブルにまたがる更新を「全部成功か、全部取り消しか」にしたいときは、InnoDB のテーブルであればトランザクションが使えます。$wpdb は失敗時に例外を投げないので、戻り値を確認して自分で例外を投げ、catch で取り消します。

<?php
global $wpdb;

$wpdb->query( 'START TRANSACTION' );
try {
    $ok = $wpdb->insert( $wpdb->prefix . 'myplugin_orders', array( 'status' => 'new' ), array( '%s' ) );
    if ( false === $ok ) {
        throw new RuntimeException( $wpdb->last_error );
    }
    // 2つ目以降の更新も同じように確認する
    $wpdb->query( 'COMMIT' );
} catch ( Throwable $e ) {
    $wpdb->query( 'ROLLBACK' );
    error_log( 'myplugin transaction rolled back: ' . $e->getMessage() );
}

データベースを書き換える処理を試すときは、事前にバックアップを取り、ステージング環境で動作を確認してから本番に入れてください。

finally の落とし穴

exit すると finally は実行されない

PHP では、try の中で exit や die が呼ばれると finally は実行されません。WordPress の wp_die()、wp_send_json_success()、wp_send_json_error()、wp_redirect() の後に書く exit も同じです。後片付けが必要な処理では、try-finally を抜けてから応答を返す形にします。

<?php
$result = myplugin_run_import(); // 内部の finally でロック解除まで終わる

if ( is_wp_error( $result ) ) {
    wp_send_json_error( array( 'message' => __( '取り込みに失敗しました。', 'myplugin' ) ), 500 );
}
wp_send_json_success( array( 'count' => (int) $result ) );

finally の中で return しない

finally に return を書くと、try や catch の戻り値、さらには処理中の例外まで上書きされて消えてしまいます。finally は後片付けだけにとどめます。

finally の中でも例外は起きうる

後片付けの処理そのものが例外を投げると、元の例外の情報が分かりにくくなります。finally には失敗しにくい単純な処理を置き、複雑な処理は別の関数に分けて戻り値を確認します。

どこで Throwable をまとめて受け止めるか

すべての関数で Throwable を catch すると、コードが読みにくくなり、想定外のバグまで隠れます。広く受け止めるのは、処理の「入口」に限るのが現実的です。

  • WP-Cron のコールバック
  • Ajax や REST API のハンドラー
  • WP-CLI のコマンド
  • 外部 SDK を呼ぶラッパー関数

入口で受け止めない例外は、PHP の致命的エラーとして扱われます。WordPress 5.2 以降はこうした致命的エラーを検知すると、「重大なエラー」の画面を出し、管理者に復旧用のリンクをメールで送る仕組み(リカバリーモード)があります。サイトが止まる前に入口で受け止め、ログに記録して処理を打ち切るほうが、利用者への影響を小さくできます。

ログの書き方と確認方法

  • ログには例外のクラス名、メッセージ、発生箇所を残す。$e->getFile() と $e->getLine() が役に立つ
  • パスワード、API キー、トークン、個人情報はログに書かない。外部 API の例外メッセージにリクエスト内容が含まれることがあるので、そのまま記録してよいか確認する
  • 本番では WP_DEBUG_DISPLAY を false にし、WP_DEBUG_LOG でファイルに記録する。ログファイルが Web から見えない設定になっているかも確認する

動作の確認は、わざと失敗させて行います。存在しない URL を渡す、不正な JSON を返すテスト用のエンドポイントを使う、PHPUnit の expectException() や WP_Error の判定で各経路をテストする、といった方法です。正常系だけを見ていると、catch や finally の中のコードは一度も実行されないまま本番に出てしまいます。

まとめ

  • WordPress 本体の関数は WP_Error や false で失敗を知らせ、例外は投げないことが多い
  • 例外は発生源に近い境界で受け止め、WP_Error に変換して呼び出し側にそろえる
  • Exception だけでなく Error 系もあるため、広く受けるなら Throwable を使う
  • ロック解除、エラーハンドラーの復元などの後片付けは finally に書く
  • exit や wp_send_json_*() の後では finally が動かない。応答は try-finally を抜けてから返す
  • 握りつぶさずにログを残し、秘密情報は記録しない

お気軽にご相談ください

お見積りへ お問い合わせへ
▲