GAS

フォームのデータを取得できませんでした。のエラーに対処した話

2024/3/12 追記

強引に発生確率を下げる方法について、2024年の記事で解説しています。
両記事ともある程度有効な手段かと思いますので、あわせてご参照ください。
フォームのデータを取得できませんでした。に対処したもう1つのシンプルな方法

0.はじめに

2022年12月頃から、Googleフォームの送信時トリガーで
以下のようなエラーが発生し不安定な状態となっているようです。
フォームのデータを取得できませんでした。のエラー画像
私の環境でも発生していたため、
一助になれればと思い対応した方法を記載しておきます。
※エラーそのものを発生させなくする方法ではありません。

 

1.何が起こったのか

フォーム送信時トリガーを設定した時、
送信時作成のイベントから中身を取り出そうとする段階でエラーを吐きます。

エラーメッセージ:Exception: フォームのデータを取得できませんでした。しばらくしてからもう一度お試しください。

ただ毎回起こるわけではなく、不定期に発生するのが厄介です。

2.根本解決の方法はあるのか

調査したところ、根本的に当エラーが発生しなくする方法は見つけられませんでした。
なので一旦「エラーが起きても問題ない形」をご提案します。

 

 

3.即時リトライを試みる方法

3-1.コード

//フォーム送信時トリガーを設定
function formRes(e) {
  try{  //通常処理
    const itemresponses = e.response.getItemResponses(); //ここで対象のエラーが発生する 
    const address = e.response.getRespondentEmail();
    main(itemresponses, address);
  }catch(e){
    //エラー時
    console.log(e);

    //対象のフォームを取得 
    const form = FormApp.getActiveForm();
    //対象フォームの回答を過去分から全て取得する
    const allItemresponses = form.getResponses();
    //最新分は配列末尾に入っているため最新分を取得 
    /** 精度重視であれば、allItemresponsesの中身をgetTimestamp()して日次比較、最新分を取得する方がベターでしょう。*/ 
    const recentResponse = allItemresponses[allItemresponses.length-1]; 
    
    //recentResponseは、tryの中のe.responseと似た扱いができる
    const itemresponses = recentResponse.getItemResponses(); 
    const address = recentResponse.getRespondentEmail();
    main(itemresponses, address); 
  }
}               
                 
function main(itemresponses, address){
  //フォーム回答を使って行いたい処理 
  //メール送信するなりチャットツールに通知するなりデータ加工して二次利用するなり
  console.log("main"); 
}

 

3-2.解説

エラーが起きるところをtryに入れ、該当のエラー発生時にcatchします。
catchしたら無理に送信時トリガーでのフォーム情報を使わず、
formオブジェクト.getResponses()で対象フォームの回答全件を取得します。
そこから最新の回答を絞り込み、本来処理に持ち込む形です。

 

3-3.デメリット

3-3-1.ほぼ同時に複数の回答があった場合の精度が保証できない

リトライ処理中に他の回答があった場合、
最新側を取得して処理に入ってしまう可能性があります。

 

3-3-2.即時でない

当然、本来のフォーム送信時トリガーよりは反応が遅くなってしまいます。

 

3-3-3.リトライ処理自体も失敗することがある

このコードをテストしている時、リトライ処理自体も1回エラーを吐きました。
Exception: Failed to retrieve form data. Please wait and try again.
失敗した直後のcatchでリトライ処理を試みると、
結構な確率でFormにアクセスする段階でエラーを吐く印象です。
「リトライ処理自体は失敗しても5回まで繰り返す」等の対策があった方が良さそうです。

 

3-4.類似の方法

似ている方法として、フォーム回答を蓄積するスプレッドシートにGASを仕込み、
シート更新に応じて最新データを取得、処理する方法があります。

デメリットとしては、フォームの設問内容を追加/削除した際、
過去の質問項目もシートに残るため、正確に情報を抜き出すことが難しそうです。
「回答があったよ!」等の情報だけが必要であれば、これで良いかもしれませんね。

4.日次で処理漏れを検知、再実行する方法

4.1コード

//フォーム送信時トリガーを設定
function formRes(e) {
  try{
    //通常処理
    const itemresponses = e.response.getItemResponses();
    const address = e.response.getRespondentEmail();
    main(itemresponses, address);

    //ログシートを取得できれば方法は何でもいい
    const logSheet = SpreadsheetApp.openById("SSのID").getSheetByName("フォーム後処理ログ");
    const lastRow = logSheet.getLastRow();
    const responseId = e.response.getId();
    const timeStamp = e.response.getTimestamp();

    //ログ書き込み
    logSheet.getRange(lastRow+1,1,1,2).setValues([timeStamp, responseId]);

  }catch(e){
    //失敗時 一応タイムリーにエラーキャッチしたいならメール飛ばす等を実装    
  }
}

function main(itemresponses, address){
  //フォーム回答を使って行いたい処理 メール送信するなりチャットツールに通知するなりデータ加工して二次利用するなり
  console.log("main");
}

//昨日分の履行をチェックする 日次トリガーにする
function dailyCheck(){
  //昨日のdayjsオブジェクト
  const yesterday = dayjs.dayjs().subtract(1,"day");

  //昨日の回答を全件取得する
  //ライブラリdayjsを使用しているが、timestampが昨日であるかを確認できれば方法は何でもいい
  const form = FormApp.openById("フォームID");
  const yesterdayResponses = form.getResponses()
                            .filter(x => dayjs.dayjs(x.getTimestamp()).isSame(yesterday,"day"));
  //昨日のフォーム回答が0件だったらここで処理終了
  if(yesterdayResponses.length==0) return;

  //ログシートからログを全件取得し、空白を除外、A列に入力されているtimestampが昨日のものを抽出
  const logSheet = SpreadsheetApp.openById("シートID").getSheetByName("フォーム後処理ログ");
  const logs = logSheet.getRange("A1:A").getValues().filter(x => x)
                .filter(x => dayjs.dayjs(x[0]).isSame(yesterday,"day"));
  
  //ログシートの情報と、フォームから直接取得した昨日の回答全件を照らし合わせ、
  //ログシートに記載のないものを抽出する=送信時にtryが失敗していたものを抽出
  const targets = yesterdayResponses.filter(x => !logs.find(y => y[1]==x.response.getId())); 
  if(targets.length == 0) return;

  //メイン処理を実行する
  for(let target of targets){
    let itemresponses = target.getItemResponses();
    let address = target.getRespondentEmail();
    main(itemresponses, address);
  }
}

 

4-2.解説

仕組み自体が少々まどろっこしいかもしれません。ゆっくり読んでください。

 

4-2-1.実行ログを取る

まず、本来の送信時トリガーでの処理時に必ず実行ログを取るようにします。
発動時に取得するイベントオブジェクト(e)から、
「回答ID」を取得してログシートに入力する仕組みです。
※シートはどこでもいいですが、回答記録されるSS上にシートを作っておくと管理上楽でしょう。

 

4-2-2.ログシートの内容と、フォーム回答全件を比較する

さて、そうすると、当該エラーを吐いた時には「回答ID」はログには残りません。
この状況を利用します。

毎日0時~1時に、対象フォームの昨日分回答を全件取得して、
その内容とログシートの内容が一致していれば、
エラーは発生していなかったという事になります。
逆に一致していなければ、ログ記録未実行=失敗しているものがあります。

コードと一緒に見ていきましょう。

 

4-2-3.実行が漏れていた分の処理を実行する

エラーで処理が実行されていなかった分は、本来処理を改めて実行しましょう。
実行するための回答情報は、前項で取得した未実行分の「回答ID」を使って取得します。
コードですと、この部分になります。
フォーム送信時トリガーと扱いが同じオブジェクトを取得できちゃいます。

4-3.メリット・デメリット

4-3-1.【メリット】即時リトライよりは精度が上がる

ほぼ同時に複数の回答があった場合の、回答の取り違えが発生しません。
これは項3で紹介した方法より良い部分ですね。

 

4-3-2.【デメリット】回答からのタイムラグが大きい

今回のケースですと、分かりやすく日次トリガーを設定して
前日分の動作に問題が無かったかを検知しますので、遅いです。
午前9時にエラーを吐いた場合、その回答への対応は翌日になります。

フォームの用途上、タイムラグが許されない場合は
数時間毎に検知できるように工夫してみたり、
エラー時にメール等で通知し日中帯は即時フォローできるようにするなど、
工夫をすればある程度カバーできる問題かと思います。

 

4-3-3.【デメリット】日次処理自体が失敗する可能性がある

項3の方法で触れましたが、こちらでも似たような処理を実行しているため
何度か実行を試みるような処理を追加した方が無難だと思います。

 

4-3-4.【デメリット】処理が煩雑になる

最後はやっぱりこれに尽きますね。地味に面倒くさい。
「修正してくれ~」と思ってしまいますね。
ですが、GASやSaaS系ツールはサーバー側の問題で不安定になりますから、
それも見越して厚めのエラー処理を当たり前に仕込むべきなのかもしれませんね。

 

5.さいごに

いかがでしたでしょうか。
私はというと、2つ目の方法を採用して日次で管理しています。

ちなみに私の環境では送信時トリガーのフォームを20個ほど管理していますが、
昨年12月中旬からポツポツとエラーが発生し、2023/1/16以来発生していません。
皆さんはいかがでしょうか?よければコメントください。
⇒2023年2月現在、やはりたまに発生するようです。

もしかすると解消されたのかもしれませんが、
Google側が不安定になると同様の事が起こるかもしれません。
時間がある際に対処しておく事をおすすめします。

また、強引にエラーハンドリングを行う方法として他でも使えるかもしれません。
参考にしていただけると幸いです。

Googleフォームでメール自動返信!GASとHTMLのコード実例をご紹介

1.はじめに

「Googleフォーム回答に対してカスタマイズした形で自動返信したい!」
そんな方へ向けてGASでの解決方法を解説した記事になります。
ぜひ参考にしてみてください。

2.作成方法

2-0.フォームを用意する

業務本番で使っているフォームにいきなり仕込むのは危険です。
本番フォームをコピーしたり、テスト用に新設して用意してください。

2-1.スクリプトエディタを開く

自動回答を仕込みたいGoogleフォームの編集画面上で、
スクリプトエディタを開いてください。
この画面にコードを書き込んでいきます。
  フォーム右上のメニューからスクリプトエディタを開く

2-2.gsファイルとhtmlファイルをエディタに作成する

今回はGAS用ファイル1つとhtmlファイル1つを使用します。
画面左側からファイルをスクリプトファイルとhtmlファイルを作成してください。
エディタ内でスクリプトファイルとHTMLファイルを作成する

2-2.コードを書く

今回私が作成したコードはこちらです。
後ほど細かく解説していきます。

//GASコード

/**
 * メイン処理
 * @param {object} e    イベントオブジェクト 質問文や回答内容を取り出せる
 */
function sendFormResponse(e){
  //フォーム回答情報をeから取得しitemresponsesに格納
  const itemResponses = e.response.getItemResponses();

  //フォームタイトルを取得
  const formTitle = FormApp.getActiveForm().getTitle();

  //回答者アドレスを収集するフォーム設定の場合、回答者アドレスを取得できる
  //収集せず固定宛先に送る場合はaddress="~~~~~~@~~~.~~"とする
  const address = e.response.getRespondentEmail();
  
  //メール設定を取得する
  const mailSettings = getMailSettings(itemResponses, formTitle, address);

  //sendMail関数を発動 メール設定を渡す
  sendMail(mailSettings);
}

/**
 * メール設定を作成
 * @param {object} itemResponses  フォーム情報
 * @param {string} formTitle    フォーム名称
 * @param {string} address     回答者メールアドレス
 * @return {object} {body:本文html, subject:メールタイトル文字列, mailTo:メールToアドレス文字列} 
 */
function getMailSettings(itemResponses, formTitle, address){
  //回答内容を「質問文:[改行][スペース]回答内容[改行]質問:・・・」となるようhtml文字列を生成
  const qaHtml = itemResponses.map(res => 
         `${res.getItem().getTitle()}:<br> ${res.getResponse()}`)
                 .join("<br><br>");

  //メール.htmlの内容に、qaHtmlとformTitleの内容を適用
  const mailBody = HtmlService.createHtmlOutputFromFile("メール.html").getContent()
                   .replace("@replace_qa@",qaHtml)
                   .replace("@replace_formtitle@", formTitle);

  const subject = `${formTitle}の回答を受け付けました。`;
  const mailTo = address;
  return {body:mailBody, subject:subject, mailTo:mailTo};
}

/**
 * メール送信
 * @param {object} mailSettings {body:本文html, subject:メールタイトル文字列, mailTo:メールToアドレス文字列}
 */
function sendMail(mailSettings){
  GmailApp.sendEmail(mailSettings.mailTo,mailSettings.subject,"",{htmlBody:mailSettings.body});
}
フォーム「@replace_formtitle@」の回答を受け付けました。<br>
後のご対応については〇〇にて回答いたします。<br>
回答まで今しばらくお待ちください。<br>
<br>
@replace_qa@
<br>
※このメールは自動返信です。<br>
3営業日以内に連絡がない場合、お手数ですが下記へお問い合わせください。<br>
問合せ先:〇〇〇@〇〇〇〇.jp

※コード内にもかなり細かく解説コメントを書いています。
邪魔になる場合はお手数ですが削除してください。

2-3.認証を通しておく

スクリプトがフォーム回答等にアクセスするので、
それを予めOKかどうか許可しておく必要があります。
一度、スクリプト起動して許可しましょう。
権限を許可します

2-4.トリガー設定

この設定を行うと、フォーム回答時にスクリプトが発動するようになります。
これで、フォーム回答すると自動的に反応するようになります。

画面左のメニューから「トリガー」を選択、
「トリガーを追加」をクリックします。

トリガーを作成します

メイン処理をフォーム送信時起動の設定にして保存しましょう。
メイン処理をフォーム回答時起動に設定して保存

3.コード解説

3-1.sendFormResponse(e)

3-1-0.この関数について

Googleフォーム回答時に自動で呼び出される関数です。

3-1-1.eについて

イベントの略称でeとしています。
「フォーム回答」というイベントが発生すると、
フォーム回答に関する情報が発生し、自動的にeに格納されると思ってください。

3-1-2.const itemResponses = e.response.getItemResponses();

eの中から各質問ごとの詳細を取り出します。
itemResponses[0].getItem().getTitle()とすれば1問目の質問文を、
itemResponses[0].getResponse()とすれば1問目の回答を得られます。
公式ドキュメント

3-1-3.const formTitle = FormApp.getActiveForm().getTitle();

フォームタイトルを取得します。
getActiveとありますが、このスクリプトと紐づいているフォームを取得します。

3-1-4.const address = e.response.getRespondentEmail();

eの中から、回答者のメールアドレスを取得します。
フォーム設定で回答者アドレスを記録する設定の場合のみ、有効です。
公式ドキュメント

3-2.getMailSettings(itemResponses, formTitle, address)

3-2-0.この関数について

メール設定情報を作成するための関数です。

3-2-1.const qaHtml = itemResponses.map…

質問と回答を、綺麗な文字列になるように処理しています。
brタグはhtmlで改行を意味します。

3-2-2.HtmlService.createHtmlOutputFromFile(“メール.html”).getContent()

メール.htmlの内容をテキストとして取得します。

3-2-3.replace(“@replace_qa@”,qaHtml)

メール.htmlの@replace_qa@部分を、qaHtmlに置き換えます。

3-2-4.return {body:mailBody, subject:subject, mailTo:mailTo}

呼び出し元のsendFormResponse関数へ情報を返します。
返った情報はmailSettingsに格納されます。

3-3.sendMail(mailSettings)

3-3-0.この関数について

mailSettingsを用いてメールを送信します。

3-3-1.GmailApp.sendEmail();

フォームのオーナーアカウントからGmailでメールを送信します。
()の中に宛先やタイトル、本文情報など必要情報を入力します。
公式:

4.まとめ

いかがでしたでしょうか。 GoogleフォームとGASは使いこなせばかなり業務で役立ちます。
ぜひコードを書いてみて、活用してみてください。

また今回のコードは、全くの初心者の方には少し難しく、
玄人の方は「こうした方がいい」と思うコードだと思います。
ご自身なりに試行錯誤しながらアレンジしてみてください。

今後も、初心者の方がコードを理解しやすいような記事や、
改善事例、コード例などを発信していきますのでチェックしてみてください。