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

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

目次

はじめに

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

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

例外処理とは

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

JavaScriptにおける例外処理

最初に、例外を考慮しない、小さな関数の動作確認から進めていきましょう。
次のコードは、価格と税率を渡すと計算結果を返却してくれる関数です。

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

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

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

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

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

8
9
const amount = getComsumptionTax(30000, 1n); // Bigint
// let amount = getComsumptionTax(30000,'1,1');

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

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

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

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

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

try/catch/finally 構文の各ブロックの記述については、 MDMの説明 (External link) を参照してください。
try ブロックの中で呼び出した先の、getComsumptionTax 関数内で発生した例外処理でも、呼び出し元のブロック内で発生した例外と同様に捕捉します。

また、数値チェックを行っている部分は、単に console.log を出力するのではなく、throw 文で意図的に例外を発生させています。
以下のように、エラーの発生原因を変更し、再度確認してみましょう。

 9
10
const amount = getComsumptionTax(30000, '1,1');
// let amount = getComsumptionTax(30000,1n);   // Bigint

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

この例では、入力データを計算に利用することを想定し、その処理を行う部分を try/catch/finally 文を用いたサンプルを用いて、実行時に TypeError が発生しても記述したコードが例外を処理できることを確認しました。

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

コールバック関数とは

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 3. コールバック例外対応
// getComsumptionTaxの高階関数化(コールバック関数を引数に受け取る関数)
const getComsumptionTax = function(price, tax, func) {
  console.log('コールバック関数を呼び出します');
  return func(price, tax);
};
const callBackFunc = function(price, tax) {
  if (isNaN(price) || isNaN(tax)) {
    throw new Error('price or tax is not a number!');
  }
  return price * tax;
};
try {
  const amount = getComsumptionTax(30000, 1.1, callBackFunc);
  console.log('計算結果:' + amount);
} 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
// 4. 非同期で、例外対応が動作しないケース
const getComsumptionTax = function(price, tax, func) {
  console.log('コールバック関数を呼び出します');
  // 処理に時間がかかることをエミュレートするため、setTimeout関数にて計算します
  setTimeout(() => {
    const amount = price * tax;
    func(amount);
  }, 1000);
};
function callBackFunc(amount) {
  console.log('計算結果:', amount);
}
try {
  getComsumptionTax(30000, 1.1, callBackFunc);
} catch (e) {
  console.log('入力内容に誤りがあります、以下のメッセージをシステム担当にご連絡ください');
  console.error(e.message);
} finally {
  console.log('処理を終了します');
}

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

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

 9
10
getComsumptionTax(30000, 1n, callBackFunc);
// getComsumptionTax(30000, 1.1, callBackFunc);   // Bigint

実行結果をみると、try/catch でエラーを捕捉できなくなっていることが確認できます。
try ブロックで getComsumptionTax()関数を呼び出していますが、計算処理が非同期の遅延処理を行うグローバルメソッド setTimeout()で実行されており、発生した例外を同期処理の中で捕捉できないことが原因です。

このような場合の対処として、JavaScript では、上記のコールバックのしくみなどを活用して非同期の中での同期処理を実現する「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
24
25
26
27
28
29
30
// 5. 非同期例外対応
// Promiseオブジェクトによる遅延処理への対応
const getComsumptionTax = function(price, tax, func) {
  console.log('コールバック関数を呼び出します');
  if (isNaN(price) || isNaN(tax)) {
    throw new Error('price or tax is not a number!');
  }
  return func(price, tax);
};
const callBackFunc = async function(price, tax) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        const amount = price * tax;
        resolve(amount);
      } catch (e) {
        reject(new Error(e.message));
      }
    }, 1000);
  });
};
try {
  const amount = await getComsumptionTax(300000, 1.1, callBackFunc);
  console.log('計算結果:', amount);
} catch (error) {
  console.log('入力内容に誤りがあります、以下のメッセージをシステム担当にご連絡ください');
  console.error(error.message);
} finally {
  console.log('処理を終了します');
}

Promise/async/await 機能により、同期的に処理が行われる点、計算時のエラーも捕捉できている点をこれまでの例などを用いて確認いただければと思います。

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 を活用した例外の処理方法はまた異なります。
その点につきましては、また機会がございましたら。