
GAS(Google Apps Script)でサクッと自動化!すごい便利ですよね。
そんなに長くないコードで面倒な手作業が消えるのは本当に魅力的です。

最近は生成AIを使えばコードを書く時間もかなり削減できますよね!
でも、その便利さが、実は一番の危険信号かもしれません。
例えば、
- テスト(10件)では動いたのに、本番(1000件)だと必ずタイムアウトする
- 担当者がシート名を変えただけで、全部壊れた
- なぜか実行が異常に遅い
そんなGAS自動化の「失敗」に、心当たりはないでしょうか?
その原因はあなたのコーディング技術の問題ではなく、ほぼ100%、コードを書き始める前の設計に問題があることがほとんどです。
この記事では、「とりあえず動けばいい」で作ってしまうことを防ぐための具体的な「設計技術」と「注意点」を徹底的に解説します。
是非とも最後までご覧ください。
失敗例1:実行時間6分の壁
6分の壁とは
Google Apps Script(GAS)には、
「1回のスクリプト実行は、最大6分まで」
という、Googleが定めたタイムリミット(実行時間の割り当て)があります。
どんなに複雑な処理でも、6分00秒を超えた瞬間に、
「Exceeded maximum execution time」
というエラーと共に、強制終了されます。
ダメなコードの例
例えば、
「AシートのC列が『未処理』の行を探し、見つけたらD列に『処理済み』と書き込みたい」
という処理を実行したいとします。
この時、6分の壁にぶつかる原因になるダメなコードの例がこちらです。
function slowProcess() {
let sheet = SpreadsheetApp.getActive().getSheetByName("Aシート");
let lastRow = sheet.getLastRow();
// データが1000件あると、このループが1000回まわる
for (let i = 2; i <= lastRow; i++) {
// ↓ ここで1000回の「通信」が発生
let status = sheet.getRange(i, 3).getValue();
if (status == "未処理") {
// ↓ 条件に合致すれば、さらに「通信」が発生
sheet.getRange(i, 4).setValue("処理済み");
}
}
}JavaScriptこのコードの致命的な問題点は、for文の中で、getValue() と setValue() を呼び出していることです。
getValue() と setValue()は、スプレッドシートという外部アプリへの通信です。
この通信は、1回あたり0.1秒〜0.5秒ほどの時間のコストがかかります。
つまり、データが1000件あれば、それだけで「1000回 × 0.1秒 = 100秒」の時間が消費されます。
データが3000件になれば、それだけで300秒(5分)です。
このように、処理件数に比例して掛かる時間が増えていく設計こそ、6分の壁にぶつかる根本原因なのです。
解決策
この問題の解決策は、GASの鉄則であるバルク処理(一括取得・一括書き込み)を徹底することです。
つまり、for文の中でスプレッドシートへの通信を行う処理をするのは控えるべきです。
具体的には この場合getValue / setValue の代わりに、複数形(s が付いた)の getValues() と setValues() をfor文の外で使います。
【改善後のコード例】
function fastProcess() {
let sheet = SpreadsheetApp.getActive().getSheetByName("Aシート");
// C列からD列まで、必要な範囲を「まとめて」指定
let range = sheet.getRange("C2:D" + sheet.getLastRow());
// 1. ループの「外」で、全データを「一度に」取得 (API通信 1回目)
let values = range.getValues(); // valuesは「2次元配列」になる
// 2. 「配列」に対してループ処理 (これはGAS内部の処理なので超高速)
for (let i = 0; i < values.length; i++) {
let status = values[i][0]; // 配列の0番目 = C列の値
if (status == "未処理") {
values[i][1] = "処理済み"; // 配列の1番目 = D列の「配列上の値」を書き換える
}
}
// 3. ループ完了後、変更済みの「配列」を「一度に」シートに書き戻す (API通信 2回目)
range.setValues(values);
}JavaScriptダメなコードでは、for文の中でgetValueやsetValueといったスプレッドシートへの通信を1000回以上発生させていました。
バルク処理の特徴は、スプレッドシートへの通信をfor文の外にあるgetValues()(1回)とsetValues()(1回)の合計2回で済ませること。
こうすることで、スプレッドシートに積み重なる通信時間を避ける事ができ、6分の壁も回避できます。
これが6分の壁を回避する、最も基本的かつ強力な設計です。
失敗例2:スパゲッティコード
スパゲッティコードとは
その名の通り、ロジック(処理の流れ)が皿の上のスパゲッティのように絡み合い、どこからどこへ繋がっているのかわからなくなった複雑なコードを指します。
これは「とりあえず動けばいい」で機能の追加・修正を繰り返した結果、処理があちこちに飛び、誰も解読が出来なくなったコードの末路です。
ダメなコードの例と解決策
処理を機能ごとに分割せず、一つの関数にすべてを詰め込むと、先述したスパゲッティーコードが完成します。
よくあるダメなコードがこちらです。
// 警告:非常に悪い例
function myFunction() {
let sheet1 = SpreadsheetApp.getActive().getSheetByName("DB");
let sheet2 = SpreadsheetApp.getActive().getSheetByName("設定");
let data = sheet1.getDataRange().getValues();
let settings = sheet2.getRange("B1:B5").getValues(); // B1が何でB2が何か不明
for (let i = 1; i < data.length; i++) {
let row = data[i];
// 【スパゲッティポイント 1】
// データ処理のループ(for)の中で、いきなりメール送信処理が始まる
if (row[5] == "申請中" && row[3] < 10000) { // col 5, col 3 が何か不明
let email = row[1];
let subject = "【承認】" + row[0];
let body = row[1] + "様の申請は承認されました。\n" + settings[0][0]; // settings[0]が何か不明
GmailApp.sendEmail(email, subject, body);
// データの状態もここで更新
sheet1.getRange(i + 1, 6).setValue("承認済み"); // ← 最悪のsetValueループ
} else if (row[5] == "申請中") {
// 【スパゲッティポイント 2】
// 別の条件のメール送信が、同じループ内に登場する
let admin = settings[1][0]; // 管理者?
let subject = "【要確認】" + row[0];
GmailApp.sendEmail(admin, subject, "...");
sheet1.getRange(i + 1, 6).setValue("要確認"); // ← 2度目のsetValueループ
}
// この後、さらに50行の別の処理が続く...
}
}JavaScriptこのコードの問題点は、本来なら別々の関数(機能)にすべき処理が、たった一つのmyFunction内のforループでごちゃ混ぜになっていることです。
結果として、row[5] や row[3] が何を意味するのか読み取れず、関数全体が「ブラックボックス」化してしまいます
こうなると一つの修正のために全体の設計から見直すという大幅な修正が必要になってしまいます。
実装する際は必ず保守性の観点から、機能ごとに関数を分けるようにしましょう。
失敗例3:ハードコーディング
ハードコーディングとは
将来変わる可能性のある情報(シート名、メールアドレスなど)を、プログラムコードの中に直接書き込む行為を指します。
具体的には"集計_2024" や "admin@example.com" といった具体的な値を、プログラムのあちこちに散りばめることです。
これをすると、何かのきっかけでコードを修正する際に、簡単な修正のはずが、1時間以上修正時間がかかってしまうといった事態になります。
ダメなコードの例
よくあるダメなコードがこちらです。
// 警告:非常に悪い例
function processReport() {
// ▼ ハードコード 1: シート名
let sheet = SpreadsheetApp.getActive().getSheetByName("集計_2024");
let data = sheet.getRange("A2:C100").getValues();
// ... 50行の処理 ...
// ▼ ハードコード 2: メールアドレス
let admin = "admin@example.com";
GmailApp.sendEmail(admin, "日報処理完了", "完了しました");
}
function archiveData() {
// ▼ ハードコード 3: 別の関数にも同じシート名
let sheet = SpreadsheetApp.getActive().getSheetByName("集計_2024");
// ▼ ハードコード 4: フォルダID
let folder = DriveApp.getFolderById("abc123xyz-THIS-IS-ID");
// ... 処理 ...
}JavaScriptこのコードの問題は、"集計_2024" や "admin@example.com" といった、将来変わる可能性のある情報が、プログラムのあちこちに点在していることです。
これでは誰かが仮に"集計_2024"というシート名を変えた瞬間、このコードは2つの関数(processReport と archiveData)で同時にエラーを吐きます。
これにより、あなたは"集計_2024"という文字列をコード全体から探し出さなければなりません。
解決策
この問題の解決策は、ハードコードされた値を、CONFIGという名前の「オブジェクト」として、コードの先頭1箇所に分離することです。
【改善後のコード例】
// ▼▼▼ 設定は、将来ここだけ直せばOK ▼▼▼
const CONFIG = {
SHEET_NAME: "集計_2024",
ADMIN_EMAIL: "admin@example.com",
TARGET_RANGE: "A2:C100",
ARCHIVE_FOLDER_ID: "abc123xyz-THIS-IS-ID"
};
// ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
function processReport() {
// ▼ ハードコーディングの代わりに、CONFIGから値を取得
let sheet = SpreadsheetApp.getActive().getSheetByName(CONFIG.SHEET_NAME);
let data = sheet.getRange(CONFIG.TARGET_RANGE).getValues();
// ... 50行の処理 ...
// ▼ CONFIGから値を取得
let admin = CONFIG.ADMIN_EMAIL;
GmailApp.sendEmail(admin, "日報処理完了", "完了しました");
}
function archiveData() {
// ▼ 別の関数でも、同じCONFIGから値を取得
let sheet = SpreadsheetApp.getActive().getSheetByName(CONFIG.SHEET_NAME);
// ▼ CONFIGから値を取得
let folder = DriveApp.getFolderById(CONFIG.ARCHIVE_FOLDER_ID);
// ... 処理 ...
}JavaScriptこの方法なら、あなただけでなく、GASに詳しくない他の運用者も簡単に設定を変える事ができます。
やることは、コードの先頭にあるCONFIGの中の値を書き換えるだけです。
これが「修正漏れ地獄」を回避する、最も基本的かつ強力な設計です。
失敗例4:トリガーの属人化
トリガーの属人化とは
トリガーの属人化とは、GASの自動実行トリガー(時間起動、フォーム送信時、カレンダー更新時など)を、特定の個人のGoogleアカウントで設定+所有している状態を指します。
何が問題なのか
最大の問題は、そのトリガーを設定した担当者がアカウントを消した瞬間に、自動化が機能しなくなることです。
しかもこれの恐ろしいところは、エラーメールが通知されないことです。
僕も昔これに遭遇してパニックになった事があります。
アカウントを削除すると、トリガーそのものが消滅するため、スクリプトは実行すらされません。
実行されていないので、当然エラーも発生しません。
こうなると、エラーメールが1通も飛ばない、幽霊トリガーが完成します。
解決策
この問題の解決策は、自動化専用アカウントでトリガーを設定することです。
こうすることで、個人の異動や退職の影響を一切受けない、安定した自動化が実現できます。
ただ注意点があるとすると、無料のGmailアカウントを自動化専用アカウントにするのは避けた方がいいです。
理由は、主に2つあります。
- GASの利用枠(Quotas)が極端に低い
-
無料アカウントは、GASのプラットフォーム全体の利用枠がビジネス利用に耐えられません。
例えば、メール送信は1日100通まで(Workspaceなら1,500通)、トリガーの総実行時間は1日90分まで(Workspaceなら6時間)と桁違いに少なく、業務が少し拡大した瞬間に上限に達して停止します。
- セキュリティーリスク
-
クライアントの重要データ(顧客リストなど)を、会社の管理外である野良アカウントで扱う行為は、重大なセキュリティーリスクになります。
失敗例5:エラーハンドリングの欠如
エラーハンドリング(
try...catch)とはtry...catchは、プログラミングにおける安全ネットです。try { ... }の中で予期せぬエラー(例:シート名が見つからない、nullのプロパティを読もうとした)が発生した瞬間に、処理をcatch { ... }に移動させ、スクリプトが停止するのを防ぐ構文です。何が問題なのか(ダメなコード)
この安全ネットを設計せず、コードをむき出しのまま実行することです。
JavaScript
JavaScript// 警告:非常に悪い例(安全ネットがない) function mainProcess() { // 1. 設定シートを取得 let sheet = SpreadsheetApp.getActive().getSheetByName("設定_2024"); // 2. 致命的なエラー // もし運用者がシート名を「設定_2025」に変えたら... // 次の行で「nullのプロパティを読めません」エラーが発生 let adminEmail = sheet.getRange("B1").getValue(); // 3. 停止 // スクリプトはここで「エラー」を出して強制終了。 // 当然、以下の「メール送信」処理には絶対に到達しない。 // ... 本来やりたかった処理 ... GmailApp.sendEmail(adminEmail, "処理完了", "..."); }このコードの問題は、エラーが起きた時に沈黙したまま停止してしまうことです。
解決策
解決策は、一つの関数の中で全体を
try...catchで囲み、エラーが起きた際の処理をcatchブロックに設計することです。【改善後のコード例】
JavaScript
JavaScriptfunction mainProcess_safe() { // ▼▼▼ ここからが安全ネット ▼▼V try { // -------------------------------- // メインの処理(本来やりたいこと) // -------------------------------- // 1. 設定シートを取得 let sheet = SpreadsheetApp.getActive().getSheetByName("設定_2024"); // 2. もしここでエラーが起きても... let adminEmail = sheet.getRange("B1").getValue(); // ... 本来やりたかった処理 ... GmailApp.sendEmail(adminEmail, "処理完了", "処理が正常に完了しました。"); // -------------------------------- // ▼▼▼ エラー発生時に、ここが実行される ▼▼▼ } catch (error) { // エラー(安全ネットが捕まえた問題)をログに残す Logger.log(error); // 開発者(管理者)に、"わかりやすい形"でエラーを通知する const ADMIN_EMAIL = "your-admin-email@example.com"; // ※これは本来CONFIG化すべき const subject = "【GAS エラーアラート】自動化スクリプトが停止しました"; const body = "スクリプト実行中に予期せぬエラーが発生しました。\n\n" + "エラーメッセージ:\n" + error.message + "\n\n" + "スタックトレース (エラー箇所):\n" + error.stack; GmailApp.sendEmail(ADMIN_EMAIL, subject, body); } }この設計により、仮にエラーが怒ってもサイレント停止せず、あなた(開発者)に即座に報告してくれるようになります。
try...catchを使う際の注意点(重要)実はこの
try...catchはエラーをなんでも検知してくれるわけではありません。この
try...catchは、スクリプトの内側で起きたエラーしか検知できません。具体的には、「失敗例1:6分の壁」や「失敗例5:GAS利用枠の超過」のようなGoogleのプラットフォーム側(外部)のエラーは検知できないのです。
これらのエラーは
try...catchの安全ネットごと強制終了されるため、catchブロックは実行されません。try...catchで検知できるもの-
シート名が
null、外部APIが応答しない、など(スクリプト内部のエラー) try...catchで検知できないもの-
実行時間6分超過、1日のメール送信上限超過、など(プラットフォーム側の強制終了)
この「検知できるエラー」と「できないエラー」の違いを理解し、「検知できないエラー(6分の壁など)」は(失敗例4で解説した)「自動化専用アカウント」で監視する。
これこそが、GASにおける完璧な設計です。
失敗例6:GAS利用枠(Quotas)の超過
GAS利用枠(Quotas)とは
先述した「実行時間6分の壁」は、GASの数ある制限(Quotas)の一つに過ぎません。
Googleは、GmailApp(メール送信)や DriveApp(ドライブ操作)といったGoogleサービスや、トリガーの総実行時間などの実行回数や処理量の上限を厳しく定めています。
- メール送信: 1,500 通 / 日
- トリガー総実行時間: 6 時間 / 日
- 外部URL取得: 100,000 回 / 日
(無料Gmailアカウントの場合、これらの枠は極端に低いです)
これらの詳細な制限は、Googleの公式ドキュメント(Googleサービスの割り当て)ですべて確認できます。
よくあるダメなコード
「1600人への一斉メール送信」という要件に対し、利用枠(Quotas)を無視して書いたコードがこちらです。
// 非常に悪い例
function sendMassEmail() {
let emailList = /* ... 1600件のメールアドレスがここに入る ... */;
// 利用枠(Quotas)を一切確認せず、盲目的にループを実行
for (let i = 0; i < emailList.length; i++) {
// 1500通までは順調に送信
// しかし、1501回目の呼び出しで...
GmailApp.sendEmail(emailList[i], "件名", "本文");
}
// ...ここでGoogleによってスクリプトが強制終了させられる
}JavaScriptこのコードの致命的な問題は、残りの利用枠を一切確認しないまま、GmailApp.sendEmailを実行し続けていることです。
その結果、1501回目の実行で上限に達した瞬間に、Googleのプラットフォーム側から強制終了させられてしまいます。
しかもこの強制終了はtry...catchでは検知できません。
スクリプトは何のログも遺言も残せず、いきなり電源をブチッと切られるのと同じです。
これにより、1501人目以降の誰にメールが届かなかったのかを特定することが極めて困難になります。
解決策
解決策は、強制終了させられる前に、残りの利用枠を監視し、自主的に処理を中断することです。
幸いなことに、GASは、メール送信枠(MailApp)に限りMailApp.getRemainingDailyQuota()という、残りの利用枠を直接確認するコマンドを用意してくれています。
ここでは、このメール送信を代表例として、具体的な改善コードを見ていきましょう。
(※注意)これはメール送信に限った解決法です。
メール以外の「トリガー総実行時間」や「外部URL取得」など、残りの枠を確認するコマンドが存在しない汎用的な制限については、PropertiesServiceを使って自前で実行回数や時間を記録する必要があります。)
【改善後のコード例】
function safeEmailSender() {
let emailList = [ /* ... 1600件のアドレス ... */ ];
for (let i = 0; i < emailList.length; i++) {
// ▼▼▼ ここからが防御コード ▼▼▼
// 「メール送信枠、あと何通残ってる?」を取得
let quotaLeft = MailApp.getRemainingDailyQuota();
// もし残り枠が10通以下になったら、安全に停止する
if (quotaLeft <= 10) {
// 「i」が「何件目まで処理したか」の記録になる
Logger.log("メール上限に達するため、" + i + "件目で処理を中断します。");
// (※上級編:ここで PropertiesService などに「i」を記録し、
// 翌日この「i」から処理を再開する設計にすると完璧)
break; // 「強制終了」ではなく、「自主的に」ループを抜ける
}
// ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
// 安全圏内なのでメールを送る
GmailApp.sendEmail(emailList[i], "件名", "本文");
}
}JavaScriptダメなコードは、Googleから強制終了させられるため、処理の制御権を失っていました。
その結果、一体どこまでメールが送信されたのかというログを残す術もありませんでした。
改善後のコードでは、getRemainingDailyQuota()で危険を察知し、if (quotaLeft <= 10)の中で強制終了させられる前に、break文で自ら処理を制御して中断しています。
これによりどこまで処理が成功したか正確に記録する(Logger.logなど)事が出来るので、誰に届かなかったか分からないという悪夢を回避できるのです。
まとめ
GASはとても手軽ですが、つい設計を怠ると、全てが無駄になります。
ここまでの失敗例の問題点とその解決策をまとめます。
- 実行時間6分の壁
-
- 問題点:
forループの中でgetValue()/setValue()を連発すると、処理件数に比例して通信時間が積み重なり、6分の上限に達してしまう。 - 解決策:
getValues()/setValues()を使い、ループの外で読み書きを各1回で済ませる(バルク処理)。
- 問題点:
- スパゲッティコード
-
- 問題点: 1つの関数に全機能を詰め込むと、ロジックが絡み合い、仕様変更の際に解読・保守が不可能になる。
- 解決策: 機能ごとに関数を分割し、関数はそれらの関数を順番に呼び出す設計図にする。
- ハードコーディング
-
- 問題点: シート名やメールアドレスといった変わる情報をコード内に直接書き込むと、修正の際に修正漏れ地獄が発生する。
- 解決策: 変わる情報は、コードの先頭に
const CONFIG = {...}という設定オブジェクトとして1箇所にまとめる。
- トリガーの属人化
-
- 問題点: トリガーを個人アカウントで設定すると、その人が退職した瞬間にトリガーが消滅し、エラー通知もなくサイレント停止する。
- 解決策: 自動化専用アカウントを組織で用意し、そのアカウントでトリガーを設定する。これにより実行権が組織のものになる。
- エラーハンドリングの欠如
-
- 問題点:
try...catchがないと、予期せぬエラーで「サイレント停止」し、管理者がエラー発生に即座に気づけない。 - 解決策: メインの処理全体を
try...catchで囲み、catchブロックで開発者(管理者)に分かりやすいエラー内容をメール通知する。
- 問題点:
- GAS利用枠(Quotas)の超過
-
- 問題点: 6分以外の制限を無視すると、
try...catchで検知できない「強制終了」が発生し、どこまで処理したか分からなくなる。 - 解決策:
MailApp.getRemainingDailyQuota()などで残りの枠を監視し、上限に達する前にbreakしてログを残す。
- 問題点: 6分以外の制限を無視すると、
これらの失敗を前提とした設計こそが、とりあえず動くコードではなく、ずっと動き続けるコードを作るコツです。
コードを書き始める前の5分の設計が、未来の5時間のデバッグを無くします。
この記事が、あなたの自動化を成功に導く設計図となれば幸いです。







