GAS『Unknown Error Occurred』の原因と対処法
- 作成日 2025.10.31
- その他
実行中にApps Script実行環境が特定の例外名を割り当てられず、汎用の「Unknown Error Occurred」として失敗するケース。典型要因は一時的なバックエンド障害、ネットワークやDNSの瞬断、レスポンスや入出力の想定外、実行コンテキストの不整合、競合やロック、レート/クォータ境界付近の挙動、HTMLサービスでのクライアント連携不整合など。再現性の有無で切り分け、詳細ログ・最小再現・指数バックオフ・チャンク処理・排他制御・入力検証で安定化させる。
- 1. エラーの意味と発生条件(まず把握)
- 2. 最短復旧フロー(再現性で分岐)
- 3. 共通ラッパで例外を可視化(どこで落ちたか)
- 4. 指数バックオフ(Unknown含む一時失敗の標準パターン)
- 5. UrlFetchの安定化(fetchAll+短タイムアウト+検証)
- 6. Sheets/Drive操作はチャンク+排他(境界症状を減らす)
- 7. 入力検証とnull安全(未知エラーの8割は前提崩れ)
- 8. Webアプリ/HTMLサービス:クライアント⇄サーバの責務分離
- 9. 長時間処理は早期終了+再開(タイムアウト連鎖を避ける)
- 10. 診断テンプレ(最小再現で“どこまで通るか”を確認)
- 11. 再発防止のログ基盤(構造化して後追い可能に)
- 12. NG→OK早見表(Unknownを出しにくくする設計)
- 13. チェックリスト(導入順)
エラーの意味と発生条件(まず把握)
・固有の例外(TypeErrorやService Errorなど)に分類できないときに一括で出る
・UrlFetch/Sheets/Drive/Gmail/カレンダー等のI/O境界、実行終了間際、トリガー発火時、WebアプリのPOST受信時に起こりやすい
・「直前まで動いていたのに突然失敗」「リトライで通る」なら一時障害の可能性が高い
最短復旧フロー(再現性で分岐)
1) もう一度実行して通るか確認(通る=一時障害の疑い)
2) Executionsログのタイムラインから失敗ステップを特定(I/O直前にログを入れる)
3) 一時復旧策:指数バックオフ付きリトライ+小さめのタイムアウト
4) 再現性がある場合:入力・サイズ・同時実行・権限・応答形式を一点ずつ検証
共通ラッパで例外を可視化(どこで落ちたか)
function safeRun(fn, ctx = {}) {
try { return fn(); }
catch (e) {
console.error('UnknownErrorTrace', {
time: new Date().toISOString(),
name: e?.name, message: e?.message, stack: e?.stack,
context: ctx
});
throw e;
}
}
// 入口例
function main() {
return safeRun(() => {
// 本処理…
}, { entry: 'main' });
}指数バックオフ(Unknown含む一時失敗の標準パターン)
function withRetry(fn, tries = 5, base = 400, max = 8000) {
let wait = base;
for (let i = 0; i < tries; i++) {
try { return fn(); }
catch (e) {
const m = String(e?.message || '');
const maybeTransient = /unknown|internal|backend|server|unavailable|timeout|rate|quota/i.test(m);
if (!maybeTransient || i === tries - 1) throw e;
Utilities.sleep(wait + Math.floor(Math.random() * 250)); // ジッタ
wait = Math.min(wait * 2, max);
}
}
}UrlFetchの安定化(fetchAll+短タイムアウト+検証)
function getJson(url) {
const opt = { muteHttpExceptions: true, followRedirects: true, timeout: 15000 };
const res = withRetry(() => UrlFetchApp.fetch(url, opt));
const code = res.getResponseCode();
if (code >= 500) throw new Error('Server ' + code);
if (code >= 400) throw new Error('Client ' + code);
const text = res.getContentText() || '';
try { return JSON.parse(text); }
catch (_) { return { raw: text }; } // 最低限のフォールバック
}
function fetchParallel(urls) {
const reqs = urls.map(u => ({ url: u, muteHttpExceptions: true, timeout: 12000 }));
return withRetry(() => UrlFetchApp.fetchAll(reqs))
.map(r => ({ code: r.getResponseCode(), body: r.getContentText() }));
}Sheets/Drive操作はチャンク+排他(境界症状を減らす)
function withSheetLock(work, waitMs = 20000) {
const lock = LockService.getDocumentLock();
if (!lock.tryLock(waitMs)) throw new Error('Lock failed');
try { return work(); } finally { lock.releaseLock(); }
}
function setValuesChunked(sh, r, c, values2d) {
if (!Array.isArray(values2d) || !Array.isArray(values2d[0])) throw new Error('2D array required');
const COLS = values2d[0].length, MAX_CELLS = 50000;
let i = 0;
return withSheetLock(() => {
while (i < values2d.length) {
const rows = Math.min(values2d.length - i, Math.floor(MAX_CELLS / COLS));
const block = values2d.slice(i, i + rows);
withRetry(() => sh.getRange(r + i, c, block.length, COLS).setValues(block));
i += rows;
Utilities.sleep(120);
}
});
}入力検証とnull安全(未知エラーの8割は前提崩れ)
const V = {
req(c, m='前提条件エラー'){ if(!c) throw new Error(m); },
str(s, name){ if(typeof s !== 'string' || !s.trim()) throw new Error(name + ' が空/不正'); return s.trim(); }
};
function writeCell(ssId, sheetName, a1, val) {
return safeRun(() => {
V.str(ssId, 'ssId'); V.str(sheetName, 'sheetName'); V.str(a1, 'a1');
const sh = SpreadsheetApp.openById(ssId).getSheetByName(sheetName);
V.req(sh, `シートが見つかりません: ${sheetName}`);
sh.getRange(a1).setValue(val ?? '');
return true;
}, { entry: 'writeCell', sheetName, a1 });
}Webアプリ/HTMLサービス:クライアント⇄サーバの責務分離
// Code.gs(サーバ側)
function doPost(e) {
return safeRun(() => {
const raw = e?.postData?.contents || '';
let obj; try { obj = JSON.parse(raw); } catch(_) { obj = { raw }; }
const result = serverProcess(obj);
return ContentService.createTextOutput(JSON.stringify({ ok:true, result }))
.setMimeType(ContentService.MimeType.JSON);
}, { entry: 'doPost', type: e?.postData?.type, len: e?.postData?.length });
}
<!-- index.html(クライアント側。失敗ハンドラ必須) -->
<script>
function send() {
google.script.run
.withSuccessHandler(res => console.log('OK', res))
.withFailureHandler(err => {
console.error('Server failed', err && err.message);
alert('失敗: ' + (err && err.message));
})
.serverProcessClient({ ping: 'pong' });
}
</script>長時間処理は早期終了+再開(タイムアウト連鎖を避ける)
function processLarge(items) {
const p = PropertiesService.getScriptProperties();
let i = Number(p.getProperty('cursor') || 0);
const start = Date.now(), LIMIT = 5*60*1000;
for (; i < items.length; i++) {
withRetry(() => heavy(items[i]));
if (Date.now() - start > LIMIT) break;
}
p.setProperty('cursor', String(i));
if (i < items.length) ScriptApp.newTrigger('resume').timeBased().after(60*1000).create();
}診断テンプレ(最小再現で“どこまで通るか”を確認)
function reproduce() {
try {
const id = 'YOUR_SHEET_ID';
const sh = SpreadsheetApp.openById(id).getSheetByName('Data');
withRetry(() => sh.getRange('A1').setValue(new Date().toISOString()));
return 'ok';
} catch (e) {
console.error('repro', { msg: e.message, stack: e.stack });
throw e;
}
}再発防止のログ基盤(構造化して後追い可能に)
function logError(e, ctx) {
try {
const sh = SpreadsheetApp.openById('LOG_SHEET_ID').getSheetByName('Errors');
sh.appendRow([new Date(), e?.name||'', e?.message||'', (e?.stack||'').slice(0,500), JSON.stringify(ctx).slice(0,500)]);
} catch (_) {
console.error('logError failed', e?.message);
}
}NG→OK早見表(Unknownを出しにくくする設計)
・NG:巨大setValuesを一度に → OK:5万セル以下にチャンク+軽いスロットリング
・NG:直列UrlFetch長時間待ち → OK:fetchAll+短タイムアウト+指数バックオフ
・NG:同時実行でappendRow競合 → OK:LockServiceで排他、まとめ書き
・NG:入力を盲信して深いプロパティ参照 → OK:null安全・型検証・フォールバック
・NG:サーバ例外をUIへ素通し → OK:withFailureHandlerでユーザー向けに整形
・NG:長時間バッチを一発勝負 → OK:カーソル保存+再開トリガーで分割実行
チェックリスト(導入順)
・再現性の有無を確認(リトライで通るか)
・失敗箇所直前に詳細ログを入れ、I/O、サイズ、同時実行を絞り込む
・withRetry(指数バックオフ+ジッタ)を適用し、タイムアウトを短めに
・setValues/Drive操作はチャンク化と排他制御を入れる
・Webアプリはサーバ側でI/O、クライアントは呼び出しと表示に限定
・長時間処理は早期終了+再開の設計に変更
・構造化ログと簡易アラート(メール/Slack等)で後追い可能にする
-
前の記事
GAS『Server Error: Internal Error』の原因と対処法 2025.10.30
-
次の記事
GAS『DriveApp cannot be used in this context』の原因と対処法 2025.11.04
コメントを書く