GASのエラーを
検知して通知する

自動化スクリプトの一番の失敗は、エラーが出ることではありません。止まっていることに数週間気づかないことです。毎朝動くはずの集計が2週間前から動いていなかった、という事故はtry-catchと通知の設計で防げます。

|対象: GAS / エラー処理 / 運用設計

Table of Contents

トリガーの失敗は誰も見ていない

手で実行しているうちは、エラーは画面に赤く出るので必ず気づきます。 問題はトリガー(決めた時刻や操作をきっかけに自動実行する仕組み)に載せてからです。 画面を見ている人がいないため、失敗しても静かに終わります。

通知が所有者にしか届かない

Googleの失敗通知はスクリプト所有者宛て。退職者のアカウントで作られていると誰も見ない

1日1通のダイジェスト

既定では即時ではなくまとめて届くため、日中の障害に当日中は気づけない

握りつぶしで成功に見える

catchして何もしないコードがあると、実行ログ上は正常終了として残る

連続失敗でトリガーが止まる

失敗が続くとトリガー自体が無効化されることがあり、復旧が遅れるほど傷が深くなる

対策は難しくありません。エラーを捕まえる場所を決める・自分たちの宛先に通知する・失敗した対象を記録するの3点です。順番に組み立てていきます。

try-catch-finallyとエラーオブジェクト

GASはJavaScriptなので、例外処理も標準のtry-catch-finallyです。 catchが受け取るエラーオブジェクトのうち、実務で使うのはmessage(何が起きたか)とstack(どこで起きたか)の2つです。

function importDailyData() {
  try {
    const sheet = SpreadsheetApp.getActive().getSheetByName("集計");
    if (!sheet) {
      // 想定外の状態は、自分でエラーを投げて止める
      throw new Error("シート「集計」が見つかりません");
    }
    // ここに本来の処理
    sheet.getRange("A1").setValue(new Date());
  } catch (e) {
    // e.message: エラー内容 / e.stack: どの関数の何行目で起きたか
    console.error("importDailyData 失敗: " + e.message + "\n" + e.stack);
    throw e; // 再送出しないと、GASの実行ログ上は「成功」として残る
  } finally {
    // 成功しても失敗しても必ず通る。後片付けはここに書く
    SpreadsheetApp.flush();
  }
}

押さえるべき点は2つあります。1つはthrow new Error()で自分からエラーを投げること。想定外の状態のまま処理を続けるより、その場で止めたほうが被害は小さくて済みます。 もう1つは、catchで処理したあとに再度throwして実行を失敗として終わらせること。これを忘れると、実行ログ上は成功扱いになり、あとから調べるときに手がかりを失います。

やってはいけない書き方

エラー処理で一番多い失敗は、書かないことではなく「書いたつもりで消してしまう」ことです。

// アンチパターン1: 握りつぶす
try {
  updateSheet();
} catch (e) {
  // 何も書かない → 失敗しても誰も気づかない
}

// アンチパターン2: メッセージだけ残して止めない
try {
  updateSheet();
} catch (e) {
  console.log("エラー"); // 何が起きたか分からず、処理は先へ進んでしまう
}

// 改善: 記録して、続けるか止めるかを明示的に決める
try {
  updateSheet();
} catch (e) {
  console.error("updateSheet 失敗: " + e.message);
  throw e; // 後続処理が前提を壊すなら止める。飛ばしてよいならthrowせず記録だけ
}

空のcatchブロックは、動作としては「エラーを無視して先へ進む」という判断を表明しています。 しかし多くの場合、書いた本人はそこまで意図していません。 エラーを捕まえたら、記録する・通知する・止めるかどうか決めるの3つを必ずセットにしてください。 「この行は失敗しても飛ばしてよい」と判断したなら、その理由をコメントで一言残しておくと、後任が迷いません。

処理を包んで失敗を自動通知する

通知の書き方を関数ごとにコピーすると、宛先を変えるだけで全箇所の修正が必要になります。 処理を受け取って実行するラッパー関数を1つ用意し、トリガーにはそれを登録する形にします。

const NOTIFY_TO = "alert@example.com";

// トリガーから呼ぶ関数をこれで包む。通知の書き方を1か所に集約できる
function runWithNotify(jobName, fn) {
  const started = new Date();
  try {
    fn();
    console.log(jobName + " 正常終了 (" + (Date.now() - started.getTime()) + "ms)");
  } catch (e) {
    notifyError(jobName, e);
    throw e; // GASの実行ログにも失敗として残す
  }
}

function notifyError(jobName, e) {
  const subject = "[GASエラー] " + jobName;
  const body = [
    "処理: " + jobName,
    "発生時刻: " + Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy-MM-dd HH:mm:ss"),
    "内容: " + e.message,
    "",
    "スタックトレース:",
    String(e.stack),
    "",
    "スクリプト: " + SpreadsheetApp.getActive().getUrl(),
  ].join("\n");

  if (shouldNotify(subject + body)) {
    MailApp.sendEmail(NOTIFY_TO, subject, body);
  }
}

// 同じ内容の通知は30分に1回までに絞る
function shouldNotify(content) {
  const cache = CacheService.getScriptCache();
  const key = "err_" + Utilities.base64EncodeWebSafe(
    Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, content)
  );
  if (cache.get(key)) return false;
  cache.put(key, "1", 1800); // 30分
  return true;
}

// トリガーにはこの関数を登録する
function dailyJob() {
  runWithNotify("日次データ取り込み", importDailyData);
}

通知本文には、処理名・発生時刻・エラー内容・スタックトレース・対象のURLを入れます。 この5点があれば、スクリプトを開く前に原因の見当が付きます。

あわせて入れているのがshouldNotify()による重複抑制です。5分おきのトリガーで外部APIが落ちていると、この対策がない限り1日に数百通のメールが届きます。 エラー内容のハッシュをCacheService(一時的な値を保存する仕組み)に入れ、同じ内容は30分に1回だけ通知する、といった単純な判定で十分効きます。 なお、GASにはメール送信の1日あたり上限があるため、通知が上限を食いつぶすと本来の業務メールまで送れなくなります。

宛先をチャットにしたい場合は、MailApp.sendEmail()の部分をSlackやGoogle ChatのWebhook送信に差し替えるだけです。通知処理が1か所にまとまっていれば、変更は数行で済みます。

1件の失敗で全体を止めない

100行を処理する途中、3行目のデータ不備で全体が止まる。これは実務でよくある困り方です。 ループの中で1件ずつcatchし、失敗した行だけを記録して次へ進む形にします。

// 1件の失敗で全体を止めず、失敗した行だけ記録して次へ進む
function processRows() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("対象一覧");
  const values = sheet.getDataRange().getValues();

  const errors = [];
  const results = [];

  for (let i = 1; i < values.length; i++) {
    const row = values[i];
    try {
      results.push([callExternalApi(row[0]), ""]);
    } catch (e) {
      // 行番号を添えて記録する。あとで再実行する範囲がこれで決まる
      errors.push("行" + (i + 1) + ": " + e.message);
      results.push(["", "失敗: " + e.message]);
    }
  }

  // 1行ずつ書くと遅いので、まとめて書き戻す
  if (results.length > 0) {
    sheet.getRange(2, 3, results.length, 2).setValues(results);
  }

  // 全体としては完了扱いだが、失敗が出たことは必ず通知する
  if (errors.length > 0) {
    notifyError(
      "対象一覧の処理(" + errors.length + "/" + results.length + "件失敗)",
      new Error(errors.slice(0, 20).join("\n"))
    );
  }
}

ここで大事なのは、失敗を飛ばしたことを必ず通知する点です。飛ばしただけで黙っていると、一部の行が永久に処理されないまま放置されます。 失敗理由をシートの列に書き戻しておけば、その列が空でない行だけを再実行する仕組みも簡単に作れます。 件数が多いときは、通知本文に全件を並べず先頭の数十件に絞り、詳細はシートを見てもらう形が現実的です。

外部API呼び出しのエラーを分類する

UrlFetchAppは既定だと400番台・500番台で例外を投げますが、そのときレスポンス本文を読めません。 APIが返しているエラー内容が分からず、調査が止まります。muteHttpExceptions: trueを付けて、自分でステータスコードを見て判断するのが定石です。

function callExternalApi(id) {
  const res = UrlFetchApp.fetch("https://api.example.com/items/" + id, {
    method: "get",
    // trueにすると、404や500でも例外を投げずにレスポンスを返す
    muteHttpExceptions: true,
  });

  const code = res.getResponseCode();
  const text = res.getContentText();

  if (code === 200) {
    return JSON.parse(text).name;
  }
  if (code === 404) {
    // 業務上ありうる状態なので、エラーにせず空で返す
    return "";
  }
  if (code === 429 || code >= 500) {
    // 時間をおけば直る可能性がある種類。リトライ対象として区別する
    throw new Error("一時エラー(" + code + "): " + text.slice(0, 200));
  }
  // それ以外はリクエスト側の問題。再実行しても同じ結果になる
  throw new Error("リクエストエラー(" + code + "): " + text.slice(0, 200));
}

分類の観点は「もう一度実行したら直るか」です。

業務上ありうる状態

404など。エラーにせず空値として扱い、処理を続ける

時間をおけば直る

429やサーバー側の500番台。リトライ対象として区別し、間隔を空けて再実行する

再実行しても直らない

認証エラーやパラメータ不正。すぐ通知して人が直す必要がある

想定外

分類できないものは通知に回す。放置して後で困るのはこの層

この分類ができていると、「一時エラーは自動リトライ、それ以外は即通知」という運用に自然につながります。 全部を同じ扱いにすると、直らないエラーを延々とリトライして時間を浪費するか、直るエラーで人を叩き起こすかのどちらかになります。

エラーログをシートに残す

GASの実行ログは一定期間で消えます。通知メールも、受信箱に埋もれると集計できません。 「先月は何回失敗したか」「どのエラーが多いか」を見たいなら、専用シートに残すのが手軽です。

// エラーを専用シートに残す。メールと違って検索・集計ができる
function logError(jobName, e, target) {
  const ss = SpreadsheetApp.getActive();
  let sheet = ss.getSheetByName("エラーログ");
  if (!sheet) {
    sheet = ss.insertSheet("エラーログ");
    sheet.appendRow(["日時", "処理", "対象", "内容", "スタックトレース"]);
    sheet.setFrozenRows(1);
  }

  sheet.appendRow([
    new Date(),
    jobName,
    target || "",
    e.message,
    String(e.stack).slice(0, 5000), // セルの文字数上限に余裕を持たせる
  ]);

  // 行が増えすぎないよう、古い行を削る
  const max = 1000;
  const extra = sheet.getLastRow() - 1 - max;
  if (extra > 0) {
    sheet.deleteRows(2, extra);
  }
}

行が無限に増えないよう、上限を決めて古い行を削る処理を入れています。 スタックトレースも長くなりがちなので、切り詰めてから書き込みます。 1か月分たまったら、エラー内容ごとの件数を数えてみてください。 大半が特定の1種類に偏っていることが多く、通知の量を減らす最短の道は、その1件を直すことだと分かります。

まとめ

GASのエラー処理は、入口の関数を1つのラッパーで包むところから始めれば十分です。 捕まえたエラーはmessageとstackを添えて自分たちの宛先へ通知し、同じ内容の連投は抑える。 ループ処理は1件ずつcatchして失敗行を記録し、飛ばしたことを必ず知らせる。 外部APIは「再実行で直るか」で分類し、リトライと即時通知を使い分ける。 この4つを入れておけば、止まっていることに気づかないという最悪のパターンは避けられます。 作った直後は問題なく動くスクリプトほど、運用に入ってからの失敗が見えにくくなります。動き出す前に仕込んでおくのが結局は一番安く済みます。

よくある質問

Googleからの失敗通知は届きますが、既定では1日1通のダイジェスト形式で、送信先はスクリプトの所有者だけです。担当者が変わっていたり、共有アカウントで作っていたりすると誰も見ていない状態になりがちです。運用に乗せるなら、スクリプト側で捕まえて自分たちの宛先へ通知する仕組みを持つほうが確実です。

個々の処理ではなく、トリガーから呼ばれる入口の関数に1か所置くのが基本です。入口でまとめて捕まえて通知し、ループの中の1件ずつの失敗は別途catchして「その行だけ飛ばす」判断をします。すべての行にtry-catchを書くとコードが読みにくくなり、逆に失敗を見落とします。

「起きても業務上問題がなく、記録も不要」と説明できるものだけです。実際にはほとんどありません。握りつぶすとしても、最低限console.error()でログには残してください。空のcatchブロックは、原因調査のときに最も時間を奪う書き方です。

エラーオブジェクトのstackプロパティに、どの関数の何行目で発生したかが入っています。通知本文にはmessageとstack、加えて処理中だった対象(行番号やファイル名など)を含めてください。この3点があれば、再現しなくても原因の当たりを付けられます。

同じエラーの連続通知を抑える仕組みを入れます。直近に送った内容と時刻をPropertiesServiceに保存し、同じ内容なら一定時間は送らない、という判定が手軽です。5分おきのトリガーで外部APIが落ちている場合、この対策がないと1日に数百通届きます。

UrlFetchAppはmuteHttpExceptionsをtrueにすると、404や500でも例外を投げずにレスポンスを返します。この場合は自分でステータスコードを確認し、必要なら自分でエラーを投げてください。指定しないと400番台・500番台で例外になるため、レスポンス本文が読めずに原因が分からなくなります。

止まらない自動化を
相談する。

動かなくなったGASの調査から、エラー通知や再実行を含む運用設計まで、業務が止まらない仕組みづくりをご相談いただけます。

GAS開発サービスを見る