※本記事はプロモーションを含みます
目次
Google スプレッドシートをバックエンドにして Google Apps Script(GAS)で日次バッチや自動同期を組んでいると、ある朝突然「トリガーの実行に失敗しました」というメールだけが届いて現場が混乱することがあります。管理画面の実行ログを見ても、どこで何のデータが原因で落ちたのか分からず、手作業で1行ずつセルを目視確認する羽目になるのは避けたい事態です。
本番運用に耐えうるスクリプトを書くためには、例外が発生してもスクリプト全体を異常終了させず、どの行で何が起きたかを記録・通知して後続処理を完走させる仕組みが欠かせません。なので、GAS エラーハンドリング try catch 通知 を軸にした堅牢設計の組み立て方について、現場のコード構成を交えて整理します。
異常終了をブロックして詳細ログを飛ばす基礎構文
まずは、処理がコケた瞬間に止まるのを防ぎ、エラーの正体を掴むための基本形です。例外を補足し、スタックトレースを含めて記録・通知する構造を先に提示します。
function executeProcessWithHandling() {/* 省略されずに維持 */}
function sendErrorNotification(taskName, message, stack) {/* 省略されずに維持 */}
この構文をいつ使うかというと、タイトルの状況のように「日次トリガーで自動実行しているのに、外部APIのタイムアウトや想定外のセル値で途中で落ち、業務が止まってしまう」というトラブルを予防したい局面です。ただし、GASは標準のままだと、1箇所でも例外が出た瞬間に処理が強制終了し、どこまで更新が完了していてどこから未処理なのかが外部から判別できなくなります。
ここでずれる点は、単に try-catch で囲ってログを出力しただけでは、スクリプト実行一覧のステータスが「成功」になってしまう点です。というのは、GASの実行ログ上では「失敗」として赤く表示されなくなるため、管理コンソールを眺めているだけでは障害が起きていることに誰も気づきません。ログを握りつぶしてしまう、いわゆるエラーの呑み込み(例外隠蔽)が発生します。
この問題を回避するには、上記コードのように catch ブロック内で即座に管理者宛ての通知(メールやWebhook経由のビジネスチャット通知)を走らせるか、運用監視ツールに引っ掛かるよう明示的に別シートへ障害フラグを書き込むといった能動的な設計を組み合わせておく必要があります。
ループ処理で1件失敗しても全体を止めないバッチの堅牢化
スプレッドシート業務で最も頻出するのが、数百行〜数千行のデータを上から順に走査し、外部ツールへの登録や計算結果の書き戻しを行うバッチ処理です。ただし、このとき関数全体を1つの try-catch で囲ってしまうと、例えば3行目でセルの型がずれてエラーが起きた瞬間に、残り数百行の処理がすべてスキップされてしまいます。
バッチの堅牢化を図るためには、ループの外側ではなく「ループの内側」に try-catch を配置するのが鉄則です。なので、1件の失敗で全体を道連れにせず、失敗した行だけをエラー列にメモして次の行の処理へ進む構造を作ります。
function processBulkRowsSafely() {/* 省略されずに維持 */}
function processIndividualRecord(id, email) {/* 省略されずに維持 */}
function notifyBatchSummary(errors) {/* 省略されずに維持 */}
このアプローチをいつ使うかというと、ユーザーが自由に入力できるスプレッドシートをマスターデータとして読み込むシステムです。というのは、全角スペースの混入、不要な改行、数値が入るべきセルへの文字列入力など、人間が手作業で編集するシートには常に予期せぬノイズが存在します。これらを完璧に正規化してからスクリプトを実行するのは運用上無理があります。
ここでずれる点は、ループ内で毎回 sheet.getRange().setValue() を呼んだり、毎回メール通知関数をキックしたりすると、処理件数が多い場合にGASの「6分制限(実行時間上限)」や「1日のメール送信制限(クォータ)」に即座に引っ掛かることです。なので、エラーが100件発生した場合にメールを100通送信しようとすれば、途中でクォータ上限に達してスクリプト自体が強制停止します。
この破綻を回避するためには、上記コードのようにエラー内容を一度配列(errorRecords)にプールしておき、ループが全て回り切った後に1通のメールや1回のチャット送信でサマリーとして通知する設計にします。ただし、シートへの書き込みも、可能であれば配列に結果を保持しておき、最後に setValues で一括反映させるのがスプレッドシート操作の基本です。
スタックトレースから発生行番号を特定して初動を早める
GASでエラーをキャッチした際、開発初心者が陥りがちなのが catch (e) { Logger.log(e); } や Logger.log(e.message); だけを出力して終わらせてしまうパターンです。というのは、これだと「TypeError: Cannot read property of undefined」という文言だけが残り、コード内のどの行で、どのオブジェクトが未定義だったのかがまったく分かりません。
保守性の高いスクリプトにするためには、e.stack を取得することでエラー発生箇所の行番号を特定できる仕組みを組み込みます。なので、スタックトレースには、エラーが呼び出された関数の階層構造と行番号が文字列として記録されています。
function traceExecutionOrigin() {/* 省略されずに維持 */}
function parentServiceLogic() {/* 省略されずに維持 */}
function childDatabaseFetch() {/* 省略されずに維持 */}
function parseAndFormatStackTrace(error) {/* 省略されずに維持 */}
この処理をいつ使うかというと、複数の .gs ファイルにコードを分割し、共通関数やユーティリティ関数を深くネストして呼び出している中規模以上のプロジェクトです。ただし、共通関数の中で落ちた場合、メッセージだけでは「誰が呼び出した引数のせいで落ちたのか」が追えなくなります。
ここでずれる点は、GASのV8ランタイムと従来のRhinoランタイムでスタックトレースの文字列フォーマットが微妙に異なること、また Web Apps(doGet/doPost)やライブラリ経由で実行された場合に、行番号の指し示す位置がエディタ上の見た目と数行ズレることがある点です。というのは、ライブラリを挟むと、ライブラリ内部の行番号が返されるため、呼び出し元のコードと混同しやすくなります。
これを回避するには、自前で正規表現パースを過剰に作り込まず、まずは error.stack の生のテキストをそのまま通知メッセージに全文貼り付ける方針を基本とすることです。ただし、削ったり綺麗に整形しようとしすぎると、肝心のエラー箇所の情報が失われる危険があります。管理者がメールを開いた瞬間、最上段に「Code:15」と明記されていれば、GASエディタの15行目に直行して即座にデバッグを開始できます。
通知スパムを防ぎながら異常を可視化する通知設計
エラーハンドリングを導入した結果、逆に運用者を苦しめるのが「深夜の通知スパム」です。例えばスプレッドシートの参照先APIがサーバーダウンした場合、1分おきに動いている監視トリガーが毎回 catch に突入し、朝起きるまでに数百通の警告メールが届いてメールボックスを埋め尽くすという事故です。
通知設計では、単に MailApp や UrlFetchApp を呼び出すだけでなく、連続する同一エラーを抑制する「クールダウン期間」や、状態管理(エラー発生時と復旧時のみ通知する)を考慮する必要があります。なので、この制御には、GASの PropertiesService を利用するのが適しています。
function robustMonitoringWithNotificationControl() {/* 省略されずに維持 */}
function performCriticalSpreadsheetTask() {/* 省略されずに維持 */}
function handleThrottledErrorNotification(error) {/* 省略されずに維持 */}
function resolveErrorStateIfRecovered() {/* 省略されずに維持 */}
この通知制御をいつ使うかというと、5分おきや10分おきなど高頻度で回る時間駆動型トリガーを設定しているバッチです。というのは、一時的な外部APIの瞬断や、Google側の内部サーバーエラー(Service invoked too many times 等)が発生した際、通知が雪だるま式に増殖して肝心のインシデント管理が麻痺するのを防ぎます。
ここでずれる点は、PropertiesService の読み書き自体にもクォータとトランザクションの競合が存在する点です。ただし、もし別のアカウントや複数のトリガーが同時にこのスクリプトを叩いた場合、プロパティの書き換えが衝突して正しくフラグが反映されないケースがあります。
この問題を回避するため、コード冒頭にあるように LockService を使って多重起動を防止した上でプロパティを更新するのが確実です。なので、スクリプトロックを取得してからエラーハンドリングとプロパティ更新を行えば、競合状態(レースコンディション)による意図しない通知漏れや連続送信を完全に排除できます。
異常系を想定したリトライとロールバックの考慮事項
単に例外をキャッチしてメールを飛ばすだけでは、エラーハンドリングとしてはまだ半分です。というのは、本番環境で真に求められるのは、「失敗したデータを中途半端な状態で残さないこと」および「一時的なネットワークエラーであれば自動で再試行して自己解決すること」です。
スプレッドシートをデータベース代わりに使っている場合、関係データベース(RDBMS)のような自動ロールバック(トランザクション破棄)機能は存在しません。ただし、スクリプトが10行目まで書き込んで11行目で落ちた場合、10行目までの書き込みはシート上に確定してしまいます。なので、これを考慮した設計パターンを頭に入れておく必要があります。
function transactionalSpreadsheetUpdate() {/* 省略されずに維持 */}
function executeFinancialCalculations(sheet) {/* 省略されずに維持 */}
この手法をいつ使うかというと、請求金額の計算、在庫数の引き落とし、ステータスの一括更新など、中途半端なデータが残ると実業務で金銭的・信用的な実害が出る領域です。というのは、処理が途中で止まり、一部のセルだけが更新された「データの不整合状態」が最も調査に時間を食います。
ここでずれる点は、GASの実行時間上限(6分)がある中で安易に Utilities.sleep を伴うリトライループを回すと、ロールバック処理を行う前に6分の時間切れでGoogleのサーバー側から強制切断される恐れがあることです。なので、強制切断された場合、catch ブロックすら実行されず、ロールバック用の復元処理も走りません。
この破綻を回避するためには、リトライ回数は最大でも2〜3回、ウェイト時間も数秒程度に抑えること、そして「そもそもセルに直接少しずつ書き込まず、すべての計算をJavaScriptのメモリ上(配列変数の中)で完結させてから、最後の1回で setValues する」という設計を徹底することです。ただし、メモリ上でエラーが起きる分には、シートのセルは一切書き換わっていないため、そもそもロールバック処理を用意する必要すらなくなります。
堅牢なGASシステムとは、単に構文として try-catch を散りばめることではありません。なので、ループのどこに境界線を引くか、エラー発生時にどんな情報を誰にどう届けるか、そして中途半端なデータ破壊をどう防ぐかという運用視点を持つことで、深夜に呼び出されない安定した自動化基盤が完成します。

