GASのtry/catchでエラーが起きてもスクリプトが止まらない!実際に使えるエラーハンドリングの書き方まとめ
はじめに
前回の記事では、`getValues()` の結果をオブジェクト配列に変換することで、`row[2]` のようなインデックス参照を `row.sendFlag` のようなプロパティ名アクセスに書き換える方法をまとめました。
コードが何をしているかがそのまま読める——という点で、実際に書いてみると想像以上にすっきりするのを感じました。
今回は前回の「ひとこと」で予告していた、`try/catch` を使ったエラーハンドリングについてまとめます。
GASのスクリプトを本番で運用していると、こういう場面に出くわします。
「メール送信スクリプトを時間ベーストリガーで毎朝動かしていたのに、データの1件だけアドレスが不正な形式で、そこで処理が止まって残り全員に送れていなかった」
気づいたときには朝の時間が過ぎていた——みたいな状況です。「1件のエラーで全体が止まる」問題を回避するのが `try/catch` の役割です。
try/catch の基本構造
まず書き方から確認します。
try {
// エラーが起きるかもしれない処理
} catch (e) {
// エラーが起きたときだけ実行される処理
}`try` の中の処理でエラーが発生すると、即座に `catch` へ処理が移ります。`catch` の引数 `e`(慣習的に `e` や `err` が使われます)にはエラーオブジェクトが渡されて、`e.message` でエラーの内容を文字列として取り出せます。
エラーが起きなければ `catch` の中は実行されません。
ループの内側に置くのが基本の使い方
一番よく使う場面は、複数行のデータを1件ずつ処理するループの中です。
function sendDailyReport() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("データ");
const values = sheet.getDataRange().getValues();
// 前回まとめたオブジェクト配列への変換
const rows = values.slice(1).map(row => ({
name: row[0],
email: row[1],
sendFlag: row[2]
}));
const targets = rows.filter(row => row.sendFlag === "送信対象");
targets.forEach(row => {
try {
GmailApp.sendEmail(
row.email,
`【ご確認】${row.name}さんへのご連絡`,
`${row.name}さん、お世話になっております。`
);
console.log(`送信成功: ${row.name}(${row.email})`);
} catch (e) {
console.error(`送信失敗: ${row.name}(${row.email})/ ${e.message}`);
}
});
}`forEach` の中の1件ずつを `try/catch` で囲んでいます。1件のメール送信でエラーが起きても、`catch` でログを残して次の行の処理に進みます——全体が止まりません。
ひとつ注意しておきたいのが、`try/catch` をループの外に置いてしまうケースです。こうすると1件でエラーが起きた時点で残りの処理もまとめて止まります。「1件ずつ処理して、エラーは受け流しながら続けたい」ときは、ループの内側に書くのがポイントです。
エラーをSlack通知と組み合わせる
エラーが起きたとき、`console.error` でログを残すだけだと実行後に気づかない場合があります。以前の記事でまとめたSlack通知と組み合わせると、エラーをリアルタイムで受け取れます。
function notifySlack(message) {
const webhookUrl = PropertiesService.getScriptProperties().getProperty("SLACK_WEBHOOK_URL");
const payload = { text: message };
UrlFetchApp.fetch(webhookUrl, {
method: "post",
contentType: "application/json",
payload: JSON.stringify(payload)
});
}
targets.forEach(row => {
try {
GmailApp.sendEmail(row.email, "件名", "本文");
} catch (e) {
const msg = `⚠️ 送信失敗: ${row.name}(${row.email})\nエラー: ${e.message}`;
console.error(msg);
notifySlack(msg);
}
});PropertiesService でWebhook URLを安全に管理する方法は、以前の記事でまとめました——こういうところでつながってきます。Slack通知があれば、スクリプトが止まらなくても「どの行でエラーが出たか」を即座に把握できます。
finally:成否に関わらず実行したい処理
`try/catch` には `finally` ブロックも追加できます。エラーが起きても起きなくても、必ず実行したい処理を書く場所です。
targets.forEach(row => {
try {
GmailApp.sendEmail(row.email, "件名", "本文");
} catch (e) {
console.error(`失敗: ${row.name} / ${e.message}`);
} finally {
// 成功・失敗どちらでも実行される
console.log(`処理完了: ${row.name}`);
}
});GASの日常的な用途では `finally` まで必要になる場面はそこまで多くはないですが、「どんな結果でも必ず実行したい処理がある」ときに使えます。処理の終了ログを確実に残したいときなどが典型です。
まとめ
`try/catch` を使ったエラーハンドリングのポイントをまとめます。
`try` の中でエラーが起きると `catch` に処理が移る。`e.message` でエラーの詳細が取れる
ループの内側に `try/catch` を書くと、1件のエラーで全体が止まらずに続けられる
`console.error` だけでなく、Slack通知と組み合わせるとエラーをリアルタイムで把握できる
`finally` はエラーの有無に関わらず必ず実行したい処理に使う
`const`・アロー関数・オブジェクト配列変換と組み合わせると、読みやすく止まりにくいコードになる
【今日の私のひとこと】
try/catchは割と便利なので、よく使います。
また、最近はAIにコーディングを任せてもよく取り入れてくれますね。
ただ、そのtryを囲む範囲を大きくし過ぎると、どこで止まったかわからない場合もあるので、注意したいところです。
さて、次は、前回サラッと出してしまった「forEach・map・filter・reduceを使い分け」についてまとめていきたいと思います。
