[OutSystems] 意外と認識されていない OutSystems の JavaScript の話

この記事は、2025-04-24(木) に行なわれた、Developer Day Tokyo 2025 のセッションで話した内容を、あらためて解説しているものです。セッションで使用したスライドは、Speaker Deck で公開しています。

**Developer Day Tokyo 2025 - OutSystems User Group** 
https://usergroups.outsystems.com/events/details/outsystems-inc-tokyo-presents-developer-day-tokyo-2025/
**セッションスライド - Speaker Deck**
https://speakerdeck.com/moriyan/outsystems-yi-wai-toren-shi-sareteinai-outsystems-no-javascript-nohua


## はじめに

OutSystems の Reactive Web App やモバイルアプリでは、クライアントアクションフローに配置した JavaScript 要素に、任意の JavaScript コードを書くことができ、さらにそのスクリプト内から、別のクライアントアクションを呼び出すことができます。このことは、多くの OutSystems 開発者の知るところでしょう。

一方で、この「スクリプト内からクライアントアクションを呼び出したとき」の、返り値や制御順序、エラー処理に関しては、あまり知られていないポイントが存在しています。それらをきちんと認識しておくことは、より理解された品質の高いアプリを開発する助けになるはずです。


## スクリプトからのクライアントアクション呼び出しの基本

クライアントアクションフローにツールボックスから配置できる JavaScript 要素（JS と書かれた黄色枠のアイコン）を開くと、任意の JavaScript コードを記述することが可能です。
左下のスコープの中には、その JavaScript 要素からアクセスできるスクリーンアクション（JavaScript 要素がスクリーンアクションの場合）とクライアントアクション（Logic タブに定義されたもの）が並ぶので、使いたいものをダブルクリックすると、下記の形式でスクリプトコードが挿入されます。
```javascript:クライアントアクション呼び出し
$actions.MyAction1()
```
![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4051737/e5492ecb-73a4-4912-a0f1-ea07ce44839b.png)

本稿の主旨からは余談ですが、この `$actions` という名前空間は、スクリーンアクションと Logic タブのクライアントアクションとで共有しています。スクリーンアクションと同じ名前のクライアントアクションを Logic タブに作れない（作ろうとするとアクション名末尾に連番が自動付与されてしまう。逆の場合も同様）のは、これが原因です。一見すると定義スコープがまったく異なるように思えるのですが、スクリプトレイヤーでは同一スコープなのです。


## 返り値の形式は出力パラメーターだけでは決まらない

![画像2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4051737/c63403b5-e407-4ce6-862f-cf8e160c505f.png)
この図のようなインターフェースで定義されているスクリーンアクションを、JavaScript 要素のスクリプトから `$actions.SayHello()` で呼び出した場合、スクリプト側で得られる返り値は、
`{GreetingText: 'こんにちは'}`
のようなオブジェクトになる場合と、Promise オブジェクトになる場合とがあります。

**Promise - JavaScript | MDN**
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise

つまり、アクションのインターフェースを見ただけでは、スクリプト側で得られる返り値の形式を確定することはできません。


## 返り値が Promise になるケース

アクションのインターフェースでは決まらないということは、アクションの中身（実装）で決まるということ。具体的には、JavaScript 要素のスクリプトから呼び出すアクションのフロー中に、下記の要素が１つ以上含まれている場合に、スクリプト側で得られる返り値は Promise になります。

* サーバーアクション
* サービスアクション
* アグリゲイトやデータアクションのリフレッシュ
* ローカルエンティティのアグリゲイト（モバイルアプリのみ）
* ローカルエンティティのエンティティアクション（モバイルアプリのみ）
* `$resolve()` を含む JavaScript 要素

下図は、スクリプト側に Promise が返るアクション定義の例です。
![画像3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4051737/a9b73f43-e28c-455e-b5f1-6be4ab967622.png)

これらに共通するのは、最終的に展開された JavaScript コードにおいて、処理の完了がコールバックのような形で、処理開始とは違う流れで受け取られるものである、ということ。
例えば、サーバーアクションやサービスアクションといったサーバー呼び出しの場合、最終的に展開される JavaScript コードでは、その完了つまりレスポンス受信は、リクエスト発行とは別の流れのイベント処理の形のコードに展開されています。


## 処理完了を待つには `$resolve()` が必要

スクリプト側に返された Promise から、アクションの出力パラメーターの値を取り出すには、Promise のメンバー関数 `then()` を使って、次のように書きます。
```javascript:アクションの返り値が Promise の場合
$actions.SayHello().then(obj => {
    $parameters.OutGreetingText = obj.GreetingText;
    $resolve();
});
```
`then()` の末尾に `$resolve()` というのが付いていますが、これを忘れると、この JavaScript 要素が配置されたアクションフローにおいて、この JavaScript 要素の出力パラメーターの値は、そのデフォルト値（Text 型の場合、デフォルト値を意図的に設定していない場合は空文字列）になります。これは、`then()` の中が実行されたとき、つまり SayHello アクションの実行が完了したときには、すでにこの JavaScript 要素を抜けてアクションフローの次の処理に移ってしまっているためです。

`$resolve()` は OutSystems が用意している関数で、これが書かれた JavaScript 要素の実行完了は、アクションフローにおいては、`$resolve()` が呼び出されるまでは待ってくれるようになります。
サーバー側設定の変更をトリガーするとか、クライアントアグリゲイトのリフレッシュを行なう、といった目的の、出力パラメーターのないアクションをスクリプトから呼び出す場合も、その返り値が Promise になる場合は、同様に `then()` で受けて `$resolve()` を実行するように書かないと、処理が完了する前に、アクションフローでは JavaScript 要素の次の処理が実行されてしまいます。
```javascript:出力パラメーターはないがアクションの返り値が Promise の場合
$actions.ChangeServerStatus(3000).then(() => {
    $resolve(); // これが呼び出されるまで JavaScript 要素の実行完了を待つ
});
```


## 呼び出し先アクションに `$resolve()` を含んだ JavaScript 要素がある場合

上で、JavaScript 要素の実行完了を `$resolve()` 呼び出しまで待ってくれるのは「アクションフローにおいて」であると書きました。どういうことかというと、例えば、下図のように、`$resolve()` のある JavaScript 要素を含んだアクションを、別の JavaScript 要素のスクリプトから呼び出した場合は、呼び出される側にある JavaScript 要素で呼び出しているサーバー処理の完了を待つことなく、左側のスクリプトには即座に Promise が返ります。つまり、完了を待ってくれるのは、アクションフローから呼び出された場合に限るというわけです。
![画像4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4051737/32572a95-7091-48c1-83e9-eff165f6d04d.png)

これは、`$resolve()` の呼び出しが処理開始とは違う流れでコールバックのような形で行なわれるから。上で、スクリプト側への返り値が Promise になる条件のひとつとして、`$resolve()` が含まれた JavaScript 要素を挙げていたのは、このためです。
```javascript:上図左側の JavaScript 要素でサーバー処理の完了を待つ書き方
$actions.PrepareServer().then(() => {
    $resolve();
});
```


## Promise が返るときはエラー処理に注意

ここまでの説明では、簡単のためにエラー処理には触れていませんでしたが、当然ながら、スクリプトから呼び出したアクション実行時エラーへの対応は、必要に応じて行なわなければなりません。

スクリプトから呼び出したアクションが Promise を返さない場合は、スクリプトで何もエラー対応されていなければ、JavaScript 要素の外側の例外ハンドラー、最終的には Global Exception Hanler で処理されます。
一方、スクリプトから呼び出したアクションが Promise を返す場合は、スクリプト上できちんとエラー処理をしておかないと、そのエラーはどこにも捕捉されません。JavaScript 要素の外側の例外ハンドラーにも、Global Exception Hanlder にも捕捉されず、プラットホームのエラーログにも出力されません。結果として、トラブル対応などの際に、原因究明が遅れて、とても困ることになります。

返り値が Promise になるアクションをスクリプトから呼び出すときは、Promise メンバー関数の `catch()`（または `then()` の第２引数）を使って、エラーをハンドリングします。エラーハンドリングの末尾は、上で紹介した `$resolve()` または `$reject()` を配置します。`$reject()` も OutSystems が用意している関数で、これを使うと、JavaScript 要素の外側にエラーが投げられて、例外ハンドラーや、なければ Global Exception Hanler でそのエラーが捕捉されるようになります。
```javascript:SayHello 実行時のエラーを例外としてレイズする
$actions.SayHello().then(obj => {
    $parameters.OutGreetingText = obj.GreetingText;
    $resolve();
}).catch(err => {
    $reject(err);
});
```
Promise を返すアクションの呼び出しでエラーが発生しても、JavaScript 要素の実行をエラーにしたくないとき、例えば、JavaScript 要素に用意したエラーメッセージ用の出力パラメーターにエラー内容を書き出して、JavaScript 要素自体は正常終了させたい場合などでは、`catch()` の中で `$resolve()` を使うようにします。
```javascript:SayHello 実行時のエラーをエラーメッセージとして JavaScript 要素の出力にする
$actions.SayHello().then(obj => {
    $parameters.OutGreetingText = obj.GreetingText;
    $resolve();
}).catch(err => {
    $parameters.OutErrorMessage = err.message;
    $resolve();
});
```
`$resolve()` を使うが、コールバック的なエラーの発生を想定する必要がないとき、例えば、`setTimeout()` を使って指定時間待機するだけの下記のようなスクリプトの場合は、スクリプト上にエラー処理を書く必要はないでしょう（`setTimeout()` の実行が失敗するというのは、実行環境自体の致命的な問題が発生しているときなので、どんな精緻なエラー処理を書いてもそれが期待通りに実行されるとは思えません）。
```javascript:入力パラメーターで指定したミリ秒だけ待つ
setTimeout($resolve, $parameters.SleepInMs);
```


## おわりに

JavaScript 要素に書いたスクリプトから、`$actions.Xxx()` の形でクライアントアクションを呼び出す場合に、あまり意識されていないけれども、知っておいた方が良いことについて解説しました。要点は次の通り

* 呼び出し先アクションの処理完了を待たずに Promise が返ってくることがある
* 呼び出し先アクションに、コールバックのような形で完了する処理が含まれている場合に Promise が返る
* Promise が返る場合は `then()` で受けて `$resolve()` で完了を待つことができる
* Promise が返る場合はエラー処理を適切に書かないとエラーが捕捉できない
* Promise の `catch()` で受けたエラーは、`$reject()` を使うと外に投げることができる

2025 年初夏現在、JavaScript 要素に書く JavaScript コードを書くのには生成 AI を使う、という OutSystems 開発者は多いと思います。ただ、本稿で示したような OutSystems 固有？の特性をきちんと踏まえたスクリプトの作成を、誰でもが生成 AI に普通に期待できるようになるのは、もう少し先のことだと思います。

本稿の内容が、楽しい OutSystems 開発の一助になれば幸いです。


## 参考資料

* **非同期JavaScriptコードを定義する - OutSystems 11 ドキュメンテーション**
https://success.outsystems.com/ja-jp/documentation/11/integration_with_external_systems/javascript/defining_asynchronous_javascript_code/