🧾事務自動化ラボ

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つを組み合わせます。

  1. 持ち時間(このコードでは4分30秒)を決め、超えたら手を止める
  2. どこまで進んだかを保存する
  3. 続きを時間主導トリガーで予約し、次の実行で保存した位置から再開する

下のコードは、これを「取引先ごとのフォルダを、Googleドライブに一括で作る」処理に当てはめたものです。

このコードですること・しないこと

すること

  • 「取引先」シートのA列の名前ごとに、指定した親フォルダの中へフォルダを作り、B列にそのURLを書く
  • 4分30秒たったら手を止め、次の行番号を保存し、1分以上あとに続きを自動で再開する
  • B列にURLがある行は飛ばす。親フォルダに同じ名前のフォルダがあれば、作らずにそれを使う
  • 20件ごとにシートへ書く。途中で止まっても、作ったフォルダのURLが失われにくくする

しないこと

  • フォルダの共有設定の変更
  • 取引先名を後から変えたときの、フォルダ名の変更
  • 親フォルダに同じ名前のフォルダが既に複数あるときの区別(最初に見つかった1つを使います)

導入手順

  1. Googleスプレッドシートを新規作成し、シートの名前を「取引先」にする
  2. A1に「取引先名」、B1に「フォルダURL」と入れ、A2から下に取引先名を入れる
  3. Googleドライブで、フォルダを作りたい場所(親フォルダ)を開き、アドレスバーのURLをコピーする
  4. スプレッドシートのメニュー「拡張機能 > Apps Script」を開き、最初から入っているコードを消して、下のコードを貼る
  5. PARENT_FOLDER_ID: '' の '' の中に、3でコピーしたURLを貼る(フォルダのIDだけでも動きます)
  6. 保存して、スプレッドシートのタブを再読み込みする
  7. メニュー「フォルダ一括作成 > 開始」を押す

初回は承認画面が出ます。承認画面での進み方と、ログイン中のアカウントを取り違えたときに出る 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つ、承認画面で一部の権限のチェックを外して許可した場合も、トリガーから動いた実行はその場で失敗し、同じエラーになります(権限のスコープ)。

直し方

  1. Apps Scriptのエディタで、導入手順で指定された初回実行用の関数を選んで実行する。送信・削除などが動く関数をむやみに選ばず、承認画面では、コードの目的と要求される権限を確認して必要な項目を許可する
  2. 使っていないトリガーが残っていれば消す。エディタ左側の時計のアイコン(トリガー)を開き、該当するトリガーの右端のメニューから削除できます
  3. トリガーは、それを作ったアカウントが承認している必要があります(承認の公式ガイド)。承認し直すのも、トリガーを作ったアカウントで行ってください

配布するコードでトリガーを作る場合、公式は、トリガーを作る前に 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)。

通知メールだけに頼ると「そもそも動かなくなった」ことには気づけません。実行ログを残し、止まったことを別のトリガーで検知する仕組みは、トリガー監視の記事 にコードごと置いています。

← 記事一覧へ