「エラーなし」で止まるGASトリガーは、原因が2通りに分かれる
relmea で踏んだ「エラーが出ないのに止まる」原因は、次の2つでした。
- 承認(OAuth)の失効:プロジェクト全体の時間主導トリガーが一斉に止まる
- トリガー関数の第1引数にイベントオブジェクトが入る:特定の1本だけ結果が残らない
どちらも、画面上は「トリガーが設定されている」状態のままです。見た目が正常なところは共通しています。
違いは範囲です。承認の失効では全部が止まり、引数の問題では該当する関数だけが結果を残しません。止まっているのが全部か1本かが、切り分けの最初の手がかりになります。
なお、「動いてはいるが、直したはずの古い挙動のまま」という3つ目の症状もあります。これはトリガーの導入がバージョン固定になっている場合の話で、別記事で書きました。この記事では扱いません。
原因1:承認(OAuth)の失効。3回起きた実例
承認の失効は、2026年7月から9月までに3回起きました。
- 1回目 2026-07-25:約5日半停止
- 2回目 2026-08-23 20:46 〜 08-24 14:04:約17時間
- 3回目 2026-09-13 09:26 〜 約16:00(前回から20日)
承認の失効だと判定する4点
実行数の画面とトリガーの挙動を見て、次の4点がそろったら承認の失効と判断しています。
- 実行数の画面で、時間主導型の最後の実行が「0秒・失敗・この実行に関するログはありません」になっている。関数が起動する前に落ちている
- それ以降、時間主導型の実行が1件も無い
- 承認が要らないシンプルトリガー(
onOpenなど)だけが成功し続けている - 読み取りだけの関数を手動実行すると「承認が必要です」が出る
1から3は状況証拠で、4点目で確定します。手動実行で承認を求められた時点で、原因はコードではなく承認だと分かります。
承認の失効が見つけにくい理由
この原因が厄介なのは、正常に見える材料が多いからです。
- トリガー一覧は全部残ったままで、設定は正常に見える
- Google からの失敗通知メールは来ない。別のプロジェクトの失敗通知は届いていたので、メールが無いことは正常の証拠になりません
- ログ用のシートも同時に止まる。実行そのものが無いので、書き込みも発生しません
catchで ERROR を書く作りの関数でも、1行も残らない- GCP プロジェクトを紐付けていないと、Cloud Logging は空のまま
エラーログがきれいに空であること自体が手がかりでした。関数が起動する前に落ちているので、関数の中の try/catch は一度も走りません。
承認失効を速く判定する方法と、復旧時の注意
ログシートを日ごとに畳む
2回目と3回目で使った判定は、ログシートを「日ごとの実行件数と、最初と最後の時刻」に畳むことでした。
relmea の正常な日は、0:02、0:40、3:02、3:46、5:25、23:20、23:54 と、終日にわたって実行が並びます。ここが途中から消えていれば、その時刻に何かが起きています。
2回目は、独立した5本のトリガーが同じ時間帯で一斉に消えていました。個別の関数のバグなら、互いに関係のない5本が同時に消えることはありません。個別の関数ではなく承認側の問題だと、この並びで読めました。
Webアプリへの問い合わせで、画面を開かずに判定する
3回目は、同じプロジェクトの Webアプリ(doGet)への問い合わせが、Google の HTML で 403 を返しました。表示は「アクセスが拒否されました/ドライブ アクセス権が必要です」です。前日には同じ URL が通っていて、再承認の直後には 200 に戻りました。
エディタの画面を開かなくても、HTTP の応答だけで承認の失効を判定できたことになります。
復旧の手順
復旧は、誰かが関数を1つ実行し、「権限を確認」から承認し直すことです。人の OAuth 同意が必要なので、relmea では AI に代行させない領域として扱っています。
既存のトリガーはそのまま動き出し、作り直しは要りませんでした。1回目は承認の5分後に、時間主導の処理が「完了」に戻ったことを実行数の画面で確かめています。2回目は、メニューから関数を手動実行して承認し直した直後から、全トリガーが復活しました。
承認する前に確認しておくこと
- 停止中に発火しなかった予約は戻りません。復旧後に自動で追いついてはくれません
- 逆に、溜まった予約が復旧時に一斉に投稿されないかは、コードで先に確かめます。relmea の配信処理は「予約の日付が今日と一致する」ことを条件にしているので、古い予約は弾かれます。これをコードで確認してから承認しました
欠けた日の埋め方
停止中のデータは、指標の性質で扱いを分けました。
- 日付を指定して取り直せる外部データ(アクセス解析)は、後から埋める
- 実行した時点の値しか取れない指標は、空けたままにする
後者を 0 で埋めると「その日の数字が0だった」という記録になり、後から今日の値で埋めると別の日の数字が混ざります。どちらも事実と違うので、欠けた日は欠けたままにしています。
見張りの穴
見張りには穴が残っています。現在の見張りは「ログが6時間無音なら鳴る」仕組みなので、発見が最大6時間遅れます。3回目は、停止から5.7時間が経った時点でまだ通知が出ていませんでした。
1時間ごとに Webアプリの 403 を検査する案は出していますが、まだ提案の段階で、実装していません。
原因2:トリガー関数の第1引数がイベントオブジェクト。2回踏んだ実例
2つ目の原因は、承認とは無関係です。時間主導トリガーは、ハンドラ関数の第1引数にイベントオブジェクトを渡します。中身は year、month、day-of-month、hour、minute、triggerUid、authMode などです。
引数なしで手動実行したときは、この引数は undefined です。だから「手動では動くのに、トリガーでは壊れる」関数ができあがります。
1回目 2026-08-11:KPI収集が例外で止まる
問題の関数は、次の形をしていました(説明のために簡略化しています)。
function collect(day) {
day = day || new Date();
// 以降で day.getFullYear() を使う
}
手動で実行すると day は undefined なので、new Date() が入って成功します。トリガーから呼ばれると、day にはイベントオブジェクトが入ります。オブジェクトは truthy なので day || new Date() はそのまま採用され、day.getFullYear() で例外になります。
この関数は成功ログしか書かない作りだったので、失敗しても痕跡がゼロでした。手がかりは「手動は成功するのに、トリガーの結果だけが残らない」という症状だけです。
同じ時刻に動いている他のトリガーは正常でした。ここが承認の失効との切り分け点です。承認が切れていれば、他のトリガーも一緒に止まります。
2回目 2026-08-21:流入集計が「成功」のまま0件
こちらは、もう一段見つけにくい形でした(同じく簡略化しています)。
function trafficCollect(maxDays) {
const days = maxDays || DEFAULT_DAYS;
// 開始日 = 今日 - days * 86400000
}
トリガーから呼ばれると maxDays にイベントオブジェクトが入り、maxDays * 86400000 が NaN になります。開始日が Invalid Date になり、日付のループが1周も回りませんでした。例外は出ず、静かに0件で終わります。
しかも集計用のタブは組み直されるので、「50日×100行を更新/今回取得0日」という INFO の成功ログが出ていました。ログ上は成功に見えます。結果として、2026-08-20 と 08-21 の2日分が欠けました。
1回目は例外で止まり、2回目は例外なしで0件のまま成功扱いになりました。原因は同じでも、現れ方が違います。Date に限らず、引数を数値や文字列として使う関数も同じところでつまずきます。
症状から原因を切り分ける早見
- 全部のトリガーが止まる。最後の実行が「0秒・失敗・ログなし」。
onOpenだけ生きている。手動実行で「承認が必要です」が出る → 承認の失効 - 1本だけ結果が残らない。手動実行は成功する。同時刻の他のトリガーは正常 → 第1引数がイベントオブジェクトの疑い
- 動いてはいるが、直したはずの古い挙動のまま → トリガーの版固定(別記事)
最初に見るのは、止まっているのが全部か1本かです。全部なら、ログシートを日ごとに畳んで一斉に消えた時刻を探します。1本だけなら、その関数の第1引数を見ます。この順番を飛ばしてコードを疑い始めると、承認の失効が原因だったときに、何を直しても1件も動きません。
relmea で決めている対策と、まだ空いている穴
引数は型で判定する
トリガーから呼ばれる関数では、引数に || で既定値を当てる書き方をやめ、型で判定する規則にしています。
// Date を期待する場合
const d = (v instanceof Date && !isNaN(v.getTime())) ? v : new Date();
// 数値を期待する場合
const n = (typeof v === 'number' && isFinite(v) && v > 0) ? v : DEFAULT_DAYS;
イベントオブジェクトは Date でも数値でもないので、どちらの判定でも外れて既定値に落ちます。
トリガーを張るときにハンドラを列挙する
新しくトリガーを張るときは、コードからハンドラ名を一覧にし、第1引数を持つ関数だけを点検します。
grep -hoE "newTrigger\('[A-Za-z_]+'\)" *.gs
全関数を読む必要はなく、トリガーに繋がっていて、かつ引数を取る関数だけを見れば足ります。
失敗しても必ず1行ログを残す
try/catch で、失敗したときにも必ず1行のログが残るようにします。成功ログしか書かない関数は、1回目のように失敗の痕跡がゼロになります。
「0件で正常終了」は WARN にする
「0件で正常終了」を INFO で書くと、2回目のように成功に見えます。0件は WARN にして、ログを見たときに目に留まる扱いにする規則にしています。
まだ空いている穴
- 承認の失効は、現状の見張りでは発見が最大6時間遅れる。1時間ごとの 403 検査は提案段階で未実装
- 承認の再同意は人の操作が要る。発見が早くなっても、復旧には人が入る
- 停止中の「実行時点の値しか取れない指標」は、後から取り戻せない
これらの対策がどれだけ効いたかを示す数字は、まだ持っていません。エラーも通知も無いときは、まず全部が止まっているか1本だけかを見て、ログが空であることそのものを手がかりにしてください。失敗時にも1行残す作りにしておけば、次に止まったときにその手がかりが残ります。
