GAS『Unknown Error Occurred』の原因と対処法
- 作成日 2025.11.05
- その他
原因が特定の例外名に分類されず、Apps Script実行環境や依存サービスの一時失敗・境界条件・前提崩れなどが重なったときに出る汎用エラー。まず再現性の有無で切り分け、詳細ログを仕込み、指数バックオフ・サイズ分割・排他制御・入力検証・再開設計で安定化させる。
- 1. エラーの意味と発生条件(まず把握)
- 2. 最短復旧フロー(再現性で分岐)
- 3. 共通ラッパで例外を可視化(どこで落ちたかを特定)
- 4. 指数バックオフ+ジッタ(Unknown含む一時失敗に効く)
- 5. UrlFetchの安定化(fetchAll+短タイムアウト+検証)
- 6. Sheets/Driveはチャンク+排他(境界症状を減らす)
- 7. 入力検証とnull安全(未知エラーの主因を潰す)
- 8. Webアプリ/HTMLサービス:責務分離と失敗ハンドラ
- 9. 長時間処理は早期終了+再開(タイムアウト連鎖を避ける)
- 10. 最小再現テンプレ(“どこまで通るか”を確認)
- 11. 構造化ログとアラート(後追いできる形に残す)
- 12. NG→OK早見表(Unknownを出しにくくする設計)
- 13. チェックリスト(導入順)
エラーの意味と発生条件(まず把握)
・固有名(TypeError/Service Error等)に収まらない失敗を包括的に表す。
・UrlFetch/Sheets/Drive/Gmailなど外部I/O境界や、実行時間の終盤、トリガー起動時に起きやすい。
・「直前まで動いた/リトライで通る」なら一時障害の疑いが高い。
最短復旧フロー(再現性で分岐)
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;
}
}指数バックオフ+ジッタ(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 transient = /unknown|internal|backend|server|unavailable|timeout|rate|quota/i.test(m);
if (!transient || 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安全(未知エラーの主因を潰す)
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 => 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『DriveApp cannot be used in this context』の原因と対処法 2025.11.04
-
次の記事
docker/k8s『Image Pull BackOff』の原因と対処法 2025.11.05
コメントを書く