kintone で例外処理をしっかり設計・実装しよう

著者名:佐藤 紅志(サイボウズ株式会社)

目次

はじめに

kintoneを利用していて、「何かエラーが起きているようだが、詳細は不明」 という状況に遭遇することはないでしょうか。
詳細がまったくわからない状態からの調査は、原因の究明に時間がかかりやすくなってしまいます。
例外処理を適切に実装することは、こうしたエラーが発生した際の原因特定に役立ちます。

この記事では、kintoneの例外処理の設計と実装について解説します。
一定の可用性と信頼性が求められるカスタマイズで、考慮したい例外処理の設計とその実装方法を紹介します。

例外処理とは

例外処理(Exception handling)は、プログラミングやシステム設計において、予期しないエラーや異常状態に対処するためのしくみです。
プログラムの実行中にエラーが発生した場合、例外処理はそのエラーを捕捉(キャッチ)し、適切な処理を行います。
「予期しないエラーは発生する」という前提のもとで、コストとのバランスを考慮しながら、さまざまな事象に対処する例外処理の設計と実装が重要です。

JavaScriptにおける例外処理

最初に、例外を考慮しない、小さな関数の動作確認から進めていきましょう。
次のコードは、単純な掛け算の結果を返す関数です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 1. 例外考慮無し
const multiply = function(a, b) {
  if (isNaN(a) || isNaN(b)) {
    console.log('a or b is not a number!');
  }
  return a * b;
};
const result = multiply(30000, '1,1');
console.log('計算結果: ' + result);
console.log('処理を終了します');

8行目で数字の1.1を渡すつもりが、誤って '1,1' という文字列を渡しています。
ブラウザーや標準入力からユーザー情報を受け取る場合によくあるミスかと思います。
このような入力ミスに対応するため、3行目のisNaN()にて、チェックを行っているので問題ないようにも見えます。

では、ブラウザーの開発者モードで実行し、結果をみてみましょう。

結果をみてみると、何も値を返さない結果として、変数resultにはNaN(非数を表すオブジェクト)が格納されています。
エラーメッセージも出力され、関数呼び出し後のconsole.log()も実行されているので問題ないようにも見えます。

次に、関数を呼び出し、引数を渡しているところを以下に変更して実行してみましょう。

8
9
// count result = multiply(30000,'1,1');
const result = multiply(30000, 1n); // Bigint

実行してみると、Uncaught TypeError: というメッセージが出力されました。
isNaN組込み数の実行時に、例外エラーが発生し、処理を中断しています。

エラーメッセージ、計算結果ともに何も出力されないため、ユーザーは何が起きたかわかりません。

では次に、JavaScriptの実行時エラーが発生する場合に備えて、例外処理を実装してみましょう。
以下のように、try/catch/finally文を用いて例外処理を実装します。

try/catch/finally構文の各ブロックの記述については、 MDNの説明 (External link) を参照してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 2. try/catch/finally で例外処理をする
const multiply = function(a, b) {
  if (isNaN(a) || isNaN(b)) {
    console.log('a or b is not a number!');
  }
  return a * b;
};
try {
  const result = multiply(30000, 1n);
  console.log('計算結果:' + result);
} catch (e) {
  console.log('入力内容に誤りがあります、以下のメッセージをシステム担当にご連絡ください');
  console.error(e.message);
} finally {
  console.log('処理を終了します');
}

実行した結果、プログラムは意図しない所で停止することなく、処理が制御されました。

tryブロックの中で呼び出した先の、multiply関数内で発生した例外処理でも、呼び出し元のブロック内で発生した例外と同様に捕捉できます。

4行目のconsole.log()throw()文に変更しますisNaN()チェックの結果がtrueになったときにも例外を発生させることができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 3. 例外処理対応(数値チェックでも例外を発生させる)
const multiply = function(a, b) {
  if (isNaN(a) || isNaN(b)) {
    throw new Error('a or b is not a number!');
  }
  return a * b;
};
try {
  const result = multiply(30000, 1n);
  console.log('計算結果:' + result);
} catch (e) {
  console.log('入力内容に誤りがあります、以下のメッセージをシステム担当にご連絡ください');
  console.error(e.message);
} finally {
  console.log('処理を終了します');
}

以下のように、エラーの発生原因を変更し、再度確認してみましょう。

 9
10
// const result = multiply(30000, 1n);   // Bigint
const result = multiply(30000, '1,1');

例外処理を実装することにより、発生する事象に即した対応と、ユーザーに伝えたい情報を出力できます。
実装者が、発生しうる例外を想定し、制御できているということがポイントになります。

この例では、入力データを計算に利用することを想定し、try/catch/finally文を使うことで、実行時にTypeErrorが発生しても例外を処理できることを確認しました。

コールバック関数における例外処理

コールバック関数とは

ここでは、あらためてコールバック関数について説明します。
すでにJavaScriptのコールバックや非同期のしくみについて理解されている方は、このセクションを読み飛ばしていただいて結構です。

次の例は、関数の第3引数に計算自体を行う関数multiplyを渡し、callTask()は指定された関数を呼び出すプログラムです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 4. コールバック関数内で例外が発生した場合
const multiply = function(a, b) {
  if (isNaN(a) || isNaN(b)) {
    throw new Error('a or b is not a number!');
  }
  return a * b;
};

const callTask = function(a, b, callback) {
  console.log('コールバック関数を呼び出します');
  return callback(a, b);
};

try {
  const result = callTask(10, '1,1', multiply);
  console.log('計算結果:' + result);
} catch (e) {
  console.log(
    '入力内容に誤りがあります、以下のメッセージをシステム担当にご連絡ください'
  );
  console.error(e.message);
} finally {
  console.log('処理を終了します');
}

これは、関数も1つの変数として取り扱えるJavaScriptの言語特性を利用したものです。
「他の関数に呼び出してもらうための関数」と呼ばれる所以の振る舞いが確認できるかと思います。

非同期処理における例外処理

上記の例では、コールバック関数を使うメリットがわかりにくいかと思います。
コールバック関数は、「処理が終わってから、この関数を実行してほしい(いったん仕事は依頼するが呼び出し元では次の作業に入りたい)」などのユースケースでよく用いられます。
ただし、非同期処理での例外処理は注意が必要となりますので、その振る舞いについて確認します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 5. 非同期で、例外対応が動作しないケース
const multiply = function(a, b) {
  if (isNaN(a) || isNaN(b)) {
    throw new Error('a or b is not a number!');
  }
  return a * b;
};

const callSyncTask = function(a, b, callback) {
  console.log('コールバック関数を呼び出します');
  setTimeout(() => {
    const result = callback(a, b);
    console.log('計算結果:' + result);
  }, 1000);
};

try {
  callSyncTask(10, 1.1, multiply);
} catch (e) {
  console.log(
    '入力内容に誤りがあります、以下のメッセージをシステム担当にご連絡ください'
  );
  console.error(e.message);
} finally {
  console.log('処理を終了します');
}

非同期の処理を行っているため、正常終了時の結果として、計算結果の出力前に処理終了のメッセージが出力されていますが、先に述べたように、これは正しい振る舞いとなります。

次に、先ほど実施したように、エラーデータを渡してみます。

18
19
// callSyncTask(10, 1.1, multiply);
callSyncTask(10, 1n, multiply); // Bigint

tryブロックでmultiplyをコールバック関数として渡していますが、実際の計算は非同期の遅延処理を行うsetTimeout()の中で非同期的に実行されています。

JavaScriptでは、非同期的に発生した例外は同期処理の中で捕捉できません。
コールバック関数のmultiplyが実行されるころには、同期処理のtryブロックの処理が終わっているからです。

そのため、非同期処理で発生した例外を捕捉するには、次のどちらかの方法で対処します。

  • 非同期の処理内でtry-catchを使用する。
  • 非同期を同期的に扱う「Promise/async/await」を使用する。

非同期の処理内で try-catch を使用する方法

非同期の処理setTimeout()内でtry-catchを使用する方法です。
こちらの方法の場合、以下のデメリットがあります。

  • setTimeout()の終了を補足できないので、callSyncTask()終了後に追加で処理を行うことができない。
  • setTimeout()内でプログラムを完結する必要があるので、処理の流れが不鮮明になりやすい。

callSyncTask()終了後に追加で処理を行うといった場合は、後述の「Promise/async/await」の方法で実現できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const multiply = function(a, b) {
  if (isNaN(a) || isNaN(b)) {
    throw new Error('a or b is not a number!');
  }
  return a * b;
};

const callSyncTask = function(a, b, callback) {
  console.log('コールバック関数を呼び出します');
  setTimeout(() => {
    try {
      const result = callback(a, b);
      console.log('計算結果:' + result);
    } catch (e) {
      console.log('入力内容に誤りがあります、以下のメッセージをシステム担当にご連絡ください');
      console.error(e.message);
    } finally {
      console.log('処理を終了します');
    }
  }, 1000);
};

callSyncTask(10, 1.1, multiply);

非同期の中での同期処理を実現する「Promise/async/await」を使用する方法

Promise/async/awaitを使うと、非同期処理を同期的な処理と同様に書くことができます。
非同期の処理内でtry-catchを使用する方法と比較しても、より自然な処理の流れで実現できます。
また、callSyncTask()処理後に追加で動作を加えることも可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const multiply = function(a, b) {
  if (isNaN(a) || isNaN(b)) {
    throw new Error('a or b is not a number!');
  }
  return a * b;
};

const callSyncTask = function(a, b, callback) {
  console.log('コールバック関数を呼び出します');
  // Promise を使って、非同期処理であることを宣言する
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        const result = callback(a, b);
        resolve(result);
      } catch (e) {
        reject(e);
      }
    }, 1000);
  });
};

// async 関数内で await を実行する
(async function() {
  try {
    // await を使って、Promise の処理が完了するまで待機する
    const result = await callSyncTask(30000, '1,1', multiply);
    console.log('計算結果:' + result);
  } catch (e) {
    console.log(
      '入力内容に誤りがあります、以下のメッセージをシステム担当にご連絡ください'
    );
    console.error(e.message);
  } finally {
    console.log('処理を終了します');
  }
})();

Promise/async/awaitにつきましては、 Promise と async/awaitでもわかりやすく解説させていただいていますので、参考にしてください。

kintone 開発における例外設計

ここまで、基本的な例外処理の振る舞いと、非同期処理についての考慮点について説明しました。
ここからは、実際のkintoneカスタマイズにおいて発生する例外についてです。

例外処理を考慮しない場合

動作の確認には、 kintone アプリストア (External link) にある 顧客リスト (External link) を使います。
該当のアプリを開き、次のコードをブラウザーの開発者モードで実行します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const url = kintone.api.url('/k/v1/record.json', true);
const body = {
  app: kintone.app.getId(),
  id: 1
};
kintone.api(url, 'GET', body,
  (resp) => {
    console.log(resp.record.会社名.value);
  }
);

デバックモードで実行し、正常に処理が行われることを確認します。

次に、idキーの値を存在しないレコードIDに変更し、エラーが起きることを確認します。
例外処理の実装はされていない為、コンソールにエラーは出力されますが、ユーザー画面には何も表示されません。

tips
補足

ここで利用している、kintone REST APIリクエストを送信するJavaScript API、kintone.api()は、以下の形式で呼び出せます。

1
kintone.api(pathOrUrl, method, params, successCallback, failureCallback);

ここまで例外の振る舞いを実際に確認していると、すでにお気付きかもしれませんが、この場合の例外処理は、failureCallback()関数部に記述できます。
さっそく実装して、確認してみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const successCallback = (resp) => {
  console.log(resp.record.会社名.value);
};
const failureCallback = (error) => {
  alert(error.message);
};

const url = kintone.api.url('/k/v1/record.json', true);
const body = {
  app: kintone.app.getId(),
  id: 1
};
kintone.api(url, 'GET', body, successCallback, failureCallback);

次の画面のように「指定したレコード(id:100)が見つかりません」というメッセージが表示されています。
メッセージが出ることで、ユーザーに何が起きているかを伝えられるようになりました。

次に、コンソールから実行しているユーザーに対して、kintoneのアクセス権を設定して動作を確認してみます。

No アクセス権 エラーハンドリング
1 アプリのアクセス権がないケース errorオブジェクト
2 レコードのアクセス権がないケース errorオブジェクト
3 フィールドのアクセス権がないケース 該当フィールドがrecordオブジェクトに返却されない

No.1、No.2では、カスタマイズコードからのアクセスは例外処理によって適切なメッセージが表示されます。

No.3では、参照しているkintoneフィールド「会社名」にアクセス権を設定しました。

kintone.api()において、failureCallback()を実装したにもかかわらず、再びエラーを捕捉できなくなりました。

この振る舞いは、以下のような動作になっています。

  1. 「会社名」フィールドに対し、アクセス権を設定し操作ユーザーの閲覧権限が外されている。
  2. record.jsonのレスポンスには、閲覧権限のあるフィールドのみが返却される。
  3. クライアントからアクセスしようとした「会社名」フィールドのjsonキーが存在しないため、実行時エラーになる。
  4. No.1およびNo.2は、kintoneサーバー上で検出できるエラーだったため、failureCallback()の処理が実行された。
  5. No.3では、api自体は正常に動作しているため、successCallback()が実行され、そこで捕捉できないエラーとなる。

したがって、この場合、クライアントサイドでも例外処理を実装する必要があります。
次のコードを実行し、結果をみてみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const successCallback = (resp) => {
  try {
    console.log(resp.record.会社名.value);
  } catch (error) {
    alert(error.message);
  }
};
const failureCallback = (error) => {
  alert(error.message);
};

const url = kintone.api.url('/k/v1/record.json', true);
const body = {
  app: kintone.app.getId(),
  id: 1
};
kintone.api(url, 'GET', body, successCallback, failureCallback);

実行した結果、エラーメッセージが表示されています。
実行時エラーのメッセージをそのまま出力しているので、わかりにくいですが、例外を捕捉し制御できていることが確認できます。

まとめ

  • 一定の信頼性が求められるカスタマイズについて、例外処理を実装しましょう。
  • 非同期処理における例外処理は、非同期部分、同期部分それぞれの例外発生を考慮し必要に応じた実装をしましょう。
  • kintoneカスタマイズにおいては、例外の事象がサーバーサイドで起きるのか、クライアントサイドで起きるのかを考慮し、それぞれ適切に実装しましょう。

おわりに

kintone.api()には、今回説明させていただいたコールバックの処理で例外に対応するほか、該当のコールバックを省略することにより、kintone.Promiseオブジェクトが返却され、Promiseの機能を用いた実装も可能です。
Promiseオブジェクトが返却される際に、asnyc/awaitを活用した例外の処理方法はまた異なります。
その点につきましては、また機会がございましたら。