GAS『Uncaught Exception』の原因と対処法
- 作成日 2025.10.27
- その他
実行中の例外がtry/catchで捕捉されずに上位まで伝播したときに出る。典型原因は「null参照・型不一致などのプログラムエラー」「権限/配信上限/レート制限などの実行環境エラー」「外部APIやネットワークの一時障害」。最短復旧は再現箇所の特定→防御的コーディング→エラー分類(再試行すべき/すべきでない)→通知と再実行の設計に落とすこと。
- 1. エラーの意味と発生条件(まず押さえる)
- 2. 最短復旧フロー(5手順)
- 3. トップレベルを安全化(共通ラッパで捕捉と通知)
- 4. イベント入口(onEdit/doPost/doGet)でのガードと有効な応答
- 5. 再試行可能・不可の分類(無限ループを防ぐ)
- 6. 入力のバリデーションとnull安全(原因の8割を潰す)
- 7. 外部API・UrlFetchは並列+タイムアウト+フォールバック
- 8. シート操作は一括・チャンク化(TypeErrorを副作用で起こさない)
- 9. クライアント連携:google.script.runの失敗ハンドラを必ず付ける
- 10. 障害の見える化(構造化ログ・サマリ行)
- 11. 最小再現テンプレ(バグを小さく切り出す)
- 12. よくあるNG→OK(クイック修正)
- 13. チェックリスト(導入順)
エラーの意味と発生条件(まず押さえる)
・try/catchで捕まえていない例外が最上位まで上がるとExecutionは失敗で終了
・onEdit/onOpen/doPost/doGet/時間トリガー/カスタム関数など、すべての入口で起こりうる
・プログラムエラー(TypeError/RangeError/ReferenceError)と、API例外(Authorization、Service error、Quota、Timeout)を分けて考える
最短復旧フロー(5手順)
1) 失敗した関数と行番号をExecutionsログ/コンソールで確認
2) 例外メッセージとstackから“最初の自分の行”を特定(原因行)
3) 入力(イベントe、シート名、ID、HTTPリクエスト)が前提通りか検証
4) try/catch+詳細ログで“再現→原因→修正”を一気に進める
5) 再発防止:ガード・バリデーション・リトライ・通知・継続設計を追加
トップレベルを安全化(共通ラッパで捕捉と通知)
/** 例外を捕捉し、詳細をログ→通知→必要に応じて再送へ回す */
function safeRun(entry, ctx = {}) {
try {
return entry();
} catch (e) {
const payload = {
when: new Date().toISOString(),
name: e && e.name,
message: e && e.message,
stack: e && e.stack,
context: ctx
};
console.error('Uncaught', payload);
// 例:管理者通知(必要ならコメントアウトを外す)
// MailApp.sendEmail('admin@example.com', 'GAS Error', JSON.stringify(payload, null, 2));
throw e; // 上位に再送(必要に応じて握りつぶすことも可能)
}
}
// 入口関数例
function main() {
return safeRun(() => {
// 本処理
}, { entry: 'main' });
}イベント入口(onEdit/doPost/doGet)でのガードと有効な応答
function onEdit(e) {
return safeRun(() => {
if (!e?.range) return; // 手動実行などは無視
const sh = e.range.getSheet();
// ...安全な本処理...
}, { entry: 'onEdit', a1: e?.range?.getA1Notation() });
}
function doPost(e) {
return safeRun(() => {
const raw = e?.postData?.contents || '';
let obj;
try { obj = JSON.parse(raw); } catch (_) { obj = { raw }; }
const result = { ok: true, received: obj };
return ContentService.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}, { entry: 'doPost', type: e?.postData?.type, len: e?.postData?.length });
}再試行可能・不可の分類(無限ループを防ぐ)
function isTransient(err) {
const m = (err && err.message) || '';
return /timeout|timed out|rate limit|quota|backend|internal|transient|service unavailable/i.test(m);
}
function withRetry(fn, tries = 4) {
let wait = 400;
for (let i = 0; i < tries; i++) {
try { return fn(); }
catch (e) {
if (!isTransient(e) || i === tries - 1) throw e;
Utilities.sleep(wait + Math.floor(Math.random() * 200));
wait = Math.min(wait * 2, 8000);
}
}
}・TypeError/ReferenceError/構文ミスは再試行不要。ネットワークやレート制限は指数バックオフで再試行に切替える
入力のバリデーションとnull安全(原因の8割を潰す)
const V = {
req(cond, msg = '前提条件エラー') { if (!cond) throw new Error(msg); },
nonEmptyString(s, name) {
const ok = typeof s === 'string' && s.trim() !== '';
if (!ok) throw new Error(name + ' が空/不正');
return s.trim();
}
};
function writeCell(sheetId, sheetName, a1, value) {
return safeRun(() => {
V.nonEmptyString(sheetId, 'sheetId');
V.nonEmptyString(sheetName, 'sheetName');
V.nonEmptyString(a1, 'a1');
const sh = SpreadsheetApp.openById(sheetId).getSheetByName(sheetName);
V.req(sh, `シートが見つかりません: ${sheetName}`);
sh.getRange(a1).setValue(value ?? '');
return true;
}, { entry: 'writeCell', sheetName, a1 });
}外部API・UrlFetchは並列+タイムアウト+フォールバック
function fetchParallel(urls) {
const reqs = urls.map(u => ({ url: u, muteHttpExceptions: true, followRedirects: true, timeout: 15000 }));
return withRetry(() => UrlFetchApp.fetchAll(reqs))
.map(r => ({ code: r.getResponseCode(), body: r.getContentText() }));
}・直列fetchの積み上がりは例外の温床。fetchAllと短めtimeoutで“落ちにくく、落ちても早く戻る”
シート操作は一括・チャンク化(TypeErrorを副作用で起こさない)
// 悪手:1セルずつ(遅い+例外が出ると中途半端)
// 良手:データは配列で処理→setValuesで一括→大きいときはチャンク
function setBlock(sh, r, c, values) {
if (!Array.isArray(values) || !Array.isArray(values[0])) throw new Error('2次元配列が必要');
const MAX_CELLS = 50000;
let i = 0;
while (i < values.length) {
const rows = Math.min(values.length - i, Math.floor(MAX_CELLS / values[0].length));
const block = values.slice(i, i + rows);
sh.getRange(r + i, c, block.length, block[0].length).setValues(block);
i += rows;
Utilities.sleep(120);
}
}クライアント連携:google.script.runの失敗ハンドラを必ず付ける
/* index.html(HtmlServiceで配信) */
<script>
function submitForm() {
google.script.run
.withSuccessHandler(res => console.log('OK', res))
.withFailureHandler(err => {
console.error('Server error', err && err.message);
alert('失敗しました: ' + (err && err.message));
})
.serverSave({ name: document.querySelector('#name').value });
}
</script>・サーバ側のUncaughtをUIに露出させない。ユーザーには要点のみ返す
障害の見える化(構造化ログ・サマリ行)
function logError(e, ctx) {
const row = [
new Date(),
e?.name || '',
e?.message || '',
(e?.stack || '').slice(0, 500),
JSON.stringify(ctx).slice(0, 500)
];
try {
const sh = SpreadsheetApp.openById('LOG_SHEET_ID').getSheetByName('Errors');
sh.appendRow(row);
} catch (_) {
console.error('log append failed', row);
}
}最小再現テンプレ(バグを小さく切り出す)
function reproduce() {
// 1) 問題の行周辺だけに削った最小コードを書く
// 2) 固定入力(擬似e、固定ID)で実行
// 3) 通る→元に差分を戻す/落ちる→さらに削る
const ss = SpreadsheetApp.openById('YOUR_SHEET_ID');
const sh = ss.getSheetByName('Data'); // 存在しないと落ちる
if (!sh) throw new Error('Dataシートが無い'); // 明確なメッセージ
return sh.getRange('A1').getValue();
}よくあるNG→OK(クイック修正)
・NG:eやシート取得のnull前提を放置 → OK:非nullガードとエラーメッセージを明示
・NG:外部APIを直列fetch → OK:fetchAll+短timeout+指数バックオフ
・NG:単一セルを連続setValue → OK:配列処理→setValues一括+チャンク
・NG:サーバ例外をUIに垂れ流し → OK:withFailureHandlerでユーザ向け文言に変換
・NG:捕捉せず終了 → OK:safeRun+logErrorで詳細ログと通知
チェックリスト(導入順)
・Executionsで失敗関数・行番号・stackを確認したか
・入口関数をsafeRunでラップし、詳細ログと通知が出るか
・プログラムエラー(Type/Reference)と環境/一時エラーを分類したか
・null/型検証・ID/シート名のバリデーションを入れたか
・UrlFetch/外部I/OはfetchAll+バックオフ設計にしたか
・シート操作は一括・チャンク化で中断に強くしたか
・クライアントはwithFailureHandlerで失敗時のUXを確保したか
・最小再現コードで原因を特定し、再発防止のテストを追加したか
-
前の記事
GAS『Cross-Origin Error』の原因と対処法 2025.10.24
-
次の記事
GAS『Unexpected End of Script』の原因と対処法 2025.10.28
コメントを書く