GASでよく出るエラーの原因と直し方(エディタでは動くのにトリガーで落ちる・6分で止まる)
エラー文をそのまま検索した方向けに、Google Apps Scriptでよく出るエラーの原因と直し方を、公式ドキュメントで確かめた範囲で整理しました。6分の上限で止まる処理は、途中で手を止めて続きから自動で再開するコードを全文置いています。
Google Apps Script(GAS)のエラーは、英語の短い一文で出ます。そのまま検索すると、原因の違う話が混ざって並びがちです。このページでは、事務の自動化でよく当たるエラーを、原因の根拠を公式ドキュメントで確かめられた範囲で並べました(確認日: 2026年9月11日)。
先に2つお断りしておきます。
- エラー文は英語表示のものを載せています。表示言語の設定によっては日本語で出ますが、日本語の文言の一覧は公式に無いため、日本語の文言は載せていません
- 公式ドキュメントで文言を確かめられなかったエラーは、見出しにエラー文を書かず、症状で書いています
早見表
| 出たもの | よくある原因 | 最初にやること |
|---|---|---|
TypeError: Cannot read properties of undefined (reading 'range') など |
トリガー用の関数をエディタの「実行」で動かした | 偽のイベントを渡すテスト用の関数から呼ぶ |
| 処理が6分前後で止まる | 1回の実行時間の上限(6分) | 読み書きをまとめる。それでも長い処理は、分けて続きから再開する |
Authorization is required to perform that action. |
承認していない権限を、トリガーから動いた実行が使おうとした | エディタで関数を1回実行して承認し直す |
onEdit でメールが送れない・別のファイルを開けない |
シンプルトリガーの制限 | インストール型トリガーに切り替える |
Service invoked too many times: ... |
1日に使える回数の上限 | 上限と残りを確かめ、回数を減らす |
Service using too much computer time for one day. |
トリガーの1日の合計実行時間の上限 | 実行の間隔を広げる |
TypeError: Cannot read properties of undefined (reading 'range')
(reading 'range') の部分は、'namedValues'・'source'・'values' などに変わることがあります。いずれも、値が undefined のままプロパティを読もうとすると出ます。ここでは、トリガー用の関数をエディタから直接実行した場合を扱います。
原因
onEdit(e) や、フォームの送信で動かす関数のように、トリガーから呼ばれる前提の関数を、エディタの「実行」ボタンで動かしたときに出ます。
引数の e(イベントオブジェクト)は、トリガーが発火したときにApps Scriptが渡すものです(トリガーの公式ガイド)。エディタの「実行」には引数を渡す欄が無いので、e は undefined のまま関数が動き、e.range を読んだ行で止まります。
function onEdit(e) {
const range = e.range; // エディタから実行すると、ここで TypeError
range.setNote('最終更新: ' + new Date());
}
この文言は、JavaScriptの実行エンジン(V8)が出すものです。Apps Scriptの現在の実行環境はV8で(V8ランタイムの概要)、同じV8で動くNode.jsで上の関数を引数なしで呼ぶと、見出しの文言がそのまま出ることを確かめています。
直し方
トリガー用の関数は、エディタから直接動かさないでください。動作を確かめたいときは、偽のイベントを作って渡す、テスト用の関数を別に置きます。
function onEdit(e) {
if (!e) {
throw new Error('onEdit はセルを編集したときに呼ばれる関数です。動作確認は testOnEdit を実行してください');
}
e.range.setNote('最終更新: ' + new Date());
}
// エディタでは、こちらを選んで「実行」する
function testOnEdit() {
const sheet = SpreadsheetApp.getActiveSheet();
onEdit({ range: sheet.getRange('A1'), source: SpreadsheetApp.getActiveSpreadsheet(), value: 'テスト' });
}
偽のイベントには、関数の中で使っているプロパティだけを入れれば足ります。シートの編集で渡されるイベントには range・source・value・oldValue などが、フォーム送信(回答先のスプレッドシート側)で渡されるイベントには namedValues・range・values などがあります(イベントオブジェクト)。
フォームの関数で (reading '0') が出る場合は、namedValues のキー(質問文)がコードと一致していないことも原因の一つです。この場合の対処は、Googleフォームの受付を自動化する記事 に書いています。
処理が6分前後で止まる(実行時間の上限)
1回の実行で使える時間は6分です。個人のGoogleアカウントでもGoogle Workspaceでも同じです(Google サービスの割り当て)。短い上限が付いている実行もあります。
| 実行のしかた | 1回の上限 | 出典 |
|---|---|---|
| スクリプトの実行(エディタ・メニュー・インストール型トリガーなど) | 6分 | 割り当て |
カスタム関数(セルに =自作の関数() と書くもの) |
30秒 | 割り当て |
シンプルトリガー(onOpen・onEdit など) |
30秒 | トリガーの公式ガイド |
まず、1セルずつ読み書きしていないかを見る
最初に疑うのは、処理の重さではなく、getValue() / setValue() を行数ぶんループで呼んでいないかです。公式のベストプラクティスには、同じ内容を1セルずつ書くと約70秒、まとめて書くと約1秒という例が載っています(Best Practices)。読むのは getValues() で1回、書くのは setValues() で1回にします。実例は 日報を月次で自動集計する記事 に置いています。
それでも長い処理は、分けて続きから再開する
フォルダの作成、PDF化、外部への問い合わせのように、1件ごとにGoogleのサービスや外部を呼ぶ処理は、まとめ書きをしても、件数が増えればいずれ6分に届きます。この場合は、1回の実行で全部やろうとせず、次の3つを組み合わせます。
- 持ち時間(このコードでは4分30秒)を決め、超えたら手を止める
- どこまで進んだかを保存する
- 続きを時間主導トリガーで予約し、次の実行で保存した位置から再開する
下のコードは、これを「取引先ごとのフォルダを、Googleドライブに一括で作る」処理に当てはめたものです。
このコードですること・しないこと
すること
- 「取引先」シートのA列の名前ごとに、指定した親フォルダの中へフォルダを作り、B列にそのURLを書く
- 4分30秒たったら手を止め、次の行番号を保存し、1分以上あとに続きを自動で再開する
- B列にURLがある行は飛ばす。親フォルダに同じ名前のフォルダがあれば、作らずにそれを使う
- 20件ごとにシートへ書く。途中で止まっても、作ったフォルダのURLが失われにくくする
しないこと
- フォルダの共有設定の変更
- 取引先名を後から変えたときの、フォルダ名の変更
- 親フォルダに同じ名前のフォルダが既に複数あるときの区別(最初に見つかった1つを使います)
導入手順
- Googleスプレッドシートを新規作成し、シートの名前を「取引先」にする
- A1に「取引先名」、B1に「フォルダURL」と入れ、A2から下に取引先名を入れる
- Googleドライブで、フォルダを作りたい場所(親フォルダ)を開き、アドレスバーのURLをコピーする
- スプレッドシートのメニュー「拡張機能 > Apps Script」を開き、最初から入っているコードを消して、下のコードを貼る
PARENT_FOLDER_ID: ''の''の中に、3でコピーしたURLを貼る(フォルダのIDだけでも動きます)- 保存して、スプレッドシートのタブを再読み込みする
- メニュー「フォルダ一括作成 > 開始」を押す
初回は承認画面が出ます。承認画面での進み方と、ログイン中のアカウントを取り違えたときに出る access_denied のことは、請求書テンプレートの記事 の導入手順に書いています。
途中で止まった回は、スプレッドシート上に「◯行目から、1分ほどあとに自動で続けます」と出ます。そのまま閉じて構いません。続きは画面を開いていなくても動きます。
コード全文
/**
* 取引先フォルダの一括作成(Google Apps Script)
* 実行時間の上限(1回6分)に当たる前に手を止め、1分以上あとに続きから自動で再開します。
* 事務自動化ラボ https://jimu-jidoka.vercel.app/
*
* 使い方(スプレッドシートの「拡張機能 > Apps Script」で開いたエディタに貼る)
* 1. 「取引先」シートの A列2行目から取引先名を入れる(1行目は見出し)
* 2. FOLDER_JOB_CFG.PARENT_FOLDER_ID に、フォルダを作る先(親フォルダ)のIDか、そのフォルダを開いたときのURLを入れる
* 3. 保存してスプレッドシートを再読み込みし、メニュー「フォルダ一括作成 > 開始」
*
* B列に作ったフォルダのURLが入ります。B列が埋まっている行は作り直しません。
* 同じ名前のフォルダが親フォルダに既にあれば、新しく作らずにそれを使います。
*/
const FOLDER_JOB_CFG = {
SHEET_NAME: '取引先',
FIRST_ROW: 2, // 1行目は見出し
NAME_COL: 1, // A列: 取引先名
URL_COL: 2, // B列: フォルダURL(スクリプトが書く)
PARENT_FOLDER_ID: '', // 親フォルダのID、またはURL
TIME_BUDGET_MS: 4.5 * 60 * 1000, // 上限6分に対して、1分半の余白を残して止める
FLUSH_EVERY: 20, // 20件ごとにシートへ書き、進み具合を保存する
RESUME_AFTER_MS: 60 * 1000, // 続きを予約するまでの最短時間
HANDLER: 'continueFolderJob', // 続きの予約(時間主導トリガー)が呼ぶ関数
PROP_NEXT_ROW: 'FOLDER_JOB_NEXT_ROW',
};
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('フォルダ一括作成')
.addItem('開始', 'startFolderJob')
.addItem('続きの予約を取り消す', 'stopFolderJob')
.addToUi();
}
/** メニューから呼ぶ。毎回最初の行から見直し、URLが空の行だけ作る */
function startFolderJob() {
const result = runFolderJob_(true);
SpreadsheetApp.getActiveSpreadsheet().toast(describeResult_(result), 'フォルダ一括作成', 10);
}
/** 続きの予約から呼ばれる。画面が無い実行なので、getUi() や toast は使わない */
function continueFolderJob() {
const result = runFolderJob_(false);
console.log(describeResult_(result));
}
/** 続きの予約と、保存した進み具合を消す */
function stopFolderJob() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) {
ss.toast('実行中です。数分おいてから押し直してください', 'フォルダ一括作成', 10);
return;
}
try {
deleteResumeTriggers_();
PropertiesService.getScriptProperties().deleteProperty(FOLDER_JOB_CFG.PROP_NEXT_ROW);
} finally {
lock.releaseLock();
}
ss.toast('続きの予約を取り消しました', 'フォルダ一括作成', 5);
}
function runFolderJob_(restart) {
const result = { status: 'done', created: 0, reused: 0, skipped: 0, nextRow: null };
const lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) {
result.status = 'busy';
return result;
}
try {
const props = PropertiesService.getScriptProperties();
// 発火し終えた予約や古い予約を先に消す(トリガーは1スクリプトにつき20個まで)
deleteResumeTriggers_();
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(FOLDER_JOB_CFG.SHEET_NAME);
if (!sheet) throw new Error('シート「' + FOLDER_JOB_CFG.SHEET_NAME + '」が見つかりません');
const parentId = parseFolderId_(FOLDER_JOB_CFG.PARENT_FOLDER_ID);
if (!parentId) throw new Error('FOLDER_JOB_CFG.PARENT_FOLDER_ID に親フォルダのIDかURLを入れてください');
const parent = DriveApp.getFolderById(parentId);
const saved = Number(props.getProperty(FOLDER_JOB_CFG.PROP_NEXT_ROW));
const startRow = restart || !(saved >= FOLDER_JOB_CFG.FIRST_ROW) ? FOLDER_JOB_CFG.FIRST_ROW : saved;
const lastRow = sheet.getLastRow();
if (lastRow < startRow) {
props.deleteProperty(FOLDER_JOB_CFG.PROP_NEXT_ROW);
return result;
}
// 読むのは1回だけ。1行ずつ getValue() すると、それだけで時間を使い切る
const numRows = lastRow - startRow + 1;
const names = sheet.getRange(startRow, FOLDER_JOB_CFG.NAME_COL, numRows, 1).getValues();
const urls = sheet.getRange(startRow, FOLDER_JOB_CFG.URL_COL, numRows, 1).getValues();
const startedAt = now_();
const cache = Object.create(null);
let pending = [];
let i = 0;
try {
for (; i < numRows; i++) {
if (now_() - startedAt >= FOLDER_JOB_CFG.TIME_BUDGET_MS) break;
const name = normalizeName_(names[i][0]);
if (!name || String(urls[i][0]).trim() !== '') {
result.skipped++;
continue;
}
const found = findOrCreateFolder_(parent, name, cache);
if (found.created) result.created++;
else result.reused++;
pending.push({ index: i, url: found.url });
if (pending.length >= FOLDER_JOB_CFG.FLUSH_EVERY) {
writeUrls_(sheet, startRow, pending);
pending = [];
props.setProperty(FOLDER_JOB_CFG.PROP_NEXT_ROW, String(startRow + i + 1));
}
}
} finally {
// 途中で例外が出ても、作ったぶんのURLは書いておく(次の実行で同じフォルダを作らないため)
writeUrls_(sheet, startRow, pending);
}
if (i < numRows) {
result.status = 'paused';
result.nextRow = startRow + i;
props.setProperty(FOLDER_JOB_CFG.PROP_NEXT_ROW, String(result.nextRow));
ScriptApp.newTrigger(FOLDER_JOB_CFG.HANDLER).timeBased().after(FOLDER_JOB_CFG.RESUME_AFTER_MS).create();
return result;
}
props.deleteProperty(FOLDER_JOB_CFG.PROP_NEXT_ROW);
return result;
} finally {
lock.releaseLock();
}
}
/** 同じ名前のフォルダがあればそれを使い、無ければ作る */
function findOrCreateFolder_(parent, name, cache) {
if (cache[name]) return { url: cache[name], created: false };
const existing = parent.getFoldersByName(name);
const folder = existing.hasNext() ? existing.next() : null;
const url = folder ? folder.getUrl() : parent.createFolder(name).getUrl();
cache[name] = url;
return { url: url, created: !folder };
}
/** 書き換えた行だけを、連続する塊ごとにまとめて書く。触っていない行は上書きしない */
function writeUrls_(sheet, startRow, pending) {
groupConsecutive_(pending).forEach((block) => {
sheet
.getRange(startRow + block[0].index, FOLDER_JOB_CFG.URL_COL, block.length, 1)
.setValues(block.map((item) => [item.url]));
});
}
function groupConsecutive_(items) {
const blocks = [];
items.forEach((item) => {
const last = blocks[blocks.length - 1];
if (last && item.index === last[last.length - 1].index + 1) last.push(item);
else blocks.push([item]);
});
return blocks;
}
function deleteResumeTriggers_() {
ScriptApp.getProjectTriggers()
.filter((trigger) => trigger.getHandlerFunction() === FOLDER_JOB_CFG.HANDLER)
.forEach((trigger) => ScriptApp.deleteTrigger(trigger));
}
/** フォルダのID、または .../folders/<ID> や ?id=<ID> を含むURLからIDを取り出す */
function parseFolderId_(value) {
const text = String(value == null ? '' : value).trim();
if (!text) return '';
const match = text.match(/\/folders\/([A-Za-z0-9_-]+)/) || text.match(/[?&]id=([A-Za-z0-9_-]+)/);
if (match) return match[1];
return /^[A-Za-z0-9_-]+$/.test(text) ? text : '';
}
/** 前後の空白を落とし、セル内改行を空白1つにする。名前の途中の空白は変えない */
function normalizeName_(value) {
return String(value == null ? '' : value).replace(/[\r\n]+/g, ' ').trim();
}
function describeResult_(result) {
if (result.status === 'busy') return '別の実行が動いています。終わってから押し直してください';
const counts = '作成 ' + result.created + '件 / 既存を利用 ' + result.reused + '件 / 対象外 ' + result.skipped + '件';
if (result.status === 'paused') return counts + '。' + result.nextRow + '行目から、1分ほどあとに自動で続けます';
return counts + '。完了しました';
}
function now_() {
return Date.now();
}
続きから再開する作りで、つまずく箇所
1. どこまで進んだかは、実行が終わると消える
変数に入れた値は、実行が終わると残りません。次に処理する行番号をスクリプトプロパティ(PropertiesService)へ保存し、次の実行で読み出します。保存できるのは1つの値につき9KB、合計500KBまでです(割り当てのページ)。保存しているのは行番号1つなので、上限には届きません。
2. 6分で打ち切られた実行では、後始末まで動くとは限らない
上限で打ち切られたときに、finally に書いた書き込みまで動くかどうかは、公式に書かれていません。そこで、動かなかった場合でも壊れない作りにしています。
- 作る前に、親フォルダの中を同じ名前で探し(
getFoldersByName)、あればそれを使う。打ち切られて「フォルダはできたのに、B列にURLが無い」行が残っても、次の実行で同じフォルダを二重に作らない - 持ち時間を6分より1分半短くして、打ち切られる前に自分で止まる
3. 続きの予約は「ちょうど1分後」ではない
after(60 * 1000) は「最短で60秒後」という指定で、実際の時間は変わり得るが、指定より早くはならない、と公式に書かれています(ClockTriggerBuilder)。続きが始まるまで数分空いても、異常ではありません。
4. 予約を作りっぱなしにすると、トリガーの上限20個に当たる
トリガーは、1ユーザー・1スクリプトにつき20個までです(割り当てのページ)。このコードは、実行のたびに最初に continueFolderJob を呼ぶ予約を消し、必要なときだけ1つ作り直します。消すのはこの関数の予約だけで、同じプロジェクトの別のトリガーには触れません。
なお、getProjectTriggers() が返すのは、そのプロジェクトのトリガーのうち自分のものだけです(ScriptApp)。共有のスプレッドシートで別の人が「開始」を押して作った予約は、自分の実行からは見えず、消えません。
5. 予約から動いた回は、画面を使えない
getUi() が扱えるのは、いま開かれているスプレッドシートの画面だけです(SpreadsheetApp)。予約から動いた回には、開いている画面がありません。そのため、メニューから押した回だけ完了の知らせをスプレッドシート上に出し、予約から動いた回は実行ログに書くだけにしています。
6. 2つの実行が重なると、同じ行を2回処理する
予約から動いている最中にメニューの「開始」を押すと、2つの実行が同じ行を処理しに行きます。ロック(LockService.getScriptLock())で先に動いている方だけを通し、後から来た方は何もせずに終わります。tryLock() は、取れなかったときに例外ではなく false を返すので、戻り値を見て分けます(Lock)。
7. 止まっている間に行を足したり消したりすると、続きの位置がずれる
開始からすべての処理が完了するまで(自動再開を待つ間も含む)、行の挿入・削除・並べ替えや、A・B列の編集をしないでください。 読み取った行位置へ後からURLを書くため、処理中に並びを変えると別の取引先の行にURLが入るおそれがあります。
保存しているのは行番号です。止まっている間に上の方へ行を挿入すると、続きは本来より手前から始まり、URLのある行を飛ばすだけで済みます。反対に行を削除すると、続きが先から始まり、何行か取り残されます。全部終わったあとに「開始」をもう一度押すと、最初の行から見直して、URLが空の行だけ作ります。
8. 例外で止まったときは、続きを予約しない
Driveの権限が無いなどで例外が出たときは、握りつぶさずにそのまま止めます。予約から動いていた回なら、Googleから失敗の通知メールが届きます。続きを予約しないのは、同じ原因で失敗し続けるのを避けるためです。原因を直してから「開始」を押してください。
検証した範囲
node --checkで構文を確認しました- Apps Scriptのサービス(SpreadsheetApp・DriveApp・PropertiesService・LockService・ScriptApp)を置き換えたNode.jsのテストで、67項目すべて合格しています。持ち時間で止まって続きを1件だけ予約すること、10件を4回の実行に分けて重複なく作り切ること、打ち切られてURLが書かれなかったフォルダを拾い直すこと、手で入れたURLを上書きしないこと、例外の直前に作ったぶんのURLを書いてから止まること、別の実行中は何もしないこと、別の用途のトリガーを消さないこと、などです。
constructor・toString・__proto__という名前でも、フォルダの作成とURLの再利用を確認しています - 実アカウントでの通し実行はしていません。 承認画面の表示、予約が実際に動き出すまでの時間、Driveでフォルダを1つ作るのにかかる時間は、テストでは確かめられない部分です。最初は取引先を数件だけ入れて動かし、Driveにフォルダができること、B列にURLが入ることを確かめてから、件数を増やしてください
Authorization is required to perform that action.
原因
公式のトラブルシューティングには、次のように書かれています(Troubleshooting)。
- トリガーから動く実行では、承認の画面を出せない
- 承認する前、または承認の期限が切れた後に発火したトリガーで起きることが多い
- コードに、まだ承認していないサービスを使う処理を足したら、承認し直す必要がある
もう1つ、承認画面で一部の権限のチェックを外して許可した場合も、トリガーから動いた実行はその場で失敗し、同じエラーになります(権限のスコープ)。
直し方
- Apps Scriptのエディタで、導入手順で指定された初回実行用の関数を選んで実行する。送信・削除などが動く関数をむやみに選ばず、承認画面では、コードの目的と要求される権限を確認して必要な項目を許可する
- 使っていないトリガーが残っていれば消す。エディタ左側の時計のアイコン(トリガー)を開き、該当するトリガーの右端のメニューから削除できます
- トリガーは、それを作ったアカウントが承認している必要があります(承認の公式ガイド)。承認し直すのも、トリガーを作ったアカウントで行ってください
配布するコードでトリガーを作る場合、公式は、トリガーを作る前に ScriptApp.requireScopes() で必要な権限がそろっているかを確かめることを勧めています。
onEdit でメールが送れない・別のファイルを開けない
関数名を onEdit や onOpen にすると、承認なしで自動的に動く「シンプルトリガー」になります。承認を経ていないぶん、次の制限があります(トリガーの公式ガイド)。
- 承認が要るサービスを使えない。たとえば、Gmailでのメール送信はできない
- 自分が入っているファイルは変更できるが、他のファイルは開けない
- 30秒を超えて動けない
- ファイルを閲覧・コメント権限で開いているときは動かない
- スクリプトからセルを書き換えても、
onEditは動かない
直し方は、関数名を onEdit 以外(例: handleEdit)にして、インストール型のトリガーに登録することです。インストール型は承認を経て動くので、メール送信のような承認が要るサービスや、他のファイルを使えます。公式ガイドも、シンプルトリガーの制限で足りないときはインストール型を案内しています。登録の手順と、onFormSubmit という名前にしたときにつまずく点は、Googleフォームの受付を自動化する記事 に書いています。
Service invoked too many times: ...
1日に使える回数の上限を超えたときの例外です。: の後にはサービス名が入ります(公式の例は Service invoked too many times: Calendar.)。
メールの場合は、1日に送れる宛先の数が上限で、個人のGoogleアカウントで100、Google Workspaceで1,500です。試用期間中のアカウントには、これとは別の制限がかかります(割り当てのページ)。残りは MailApp.getRemainingDailyQuota() で確かめられます。
Service invoked too many times in a short time: ... のように in a short time が付く場合は、1日の上限ではなく、短い間に呼びすぎたときのものです。公式は、呼び出しの間に Utilities.sleep(1000) を入れるよう案内しています。
Service using too much computer time for one day.
トリガーから動いた実行の合計時間が、1日の上限を超えたときの例外です。上限は、個人のGoogleアカウントで1日90分、Google Workspaceで1日6時間です(割り当てのページ)。公式にも、トリガーで動くスクリプトで最もよく起きると書かれています。1分ごとのトリガーだと1回あたり何秒まで使えるかの計算は、GASのトリガーが止まっても気づけるようにする記事 に書いています。
上のフォルダ作成のコードも、予約から動いた回はこの合計に数えられます。
エラーが、画面ではなくメールで届いた場合
トリガーから動いた処理が失敗しても、画面にはエラーが出ません。代わりに、Googleから失敗の通知メールが届きます。エラー文と行番号は、エディタ左側の実行履歴(英語表示では Executions)で1回ずつ開いて確かめられます(Troubleshooting)。
通知メールだけに頼ると「そもそも動かなくなった」ことには気づけません。実行ログを残し、止まったことを別のトリガーで検知する仕組みは、トリガー監視の記事 にコードごと置いています。