Node.jsで終了ステータスコードを使うなら押さえておきたいasyncと終了ステータスコードの話

# 概要

Node.jsに限らず、ローカル実行向けのプログラムと作っていると、度々コマンドをチェーンして実行する必要が出てきます。
その際、処理を実行していいかの判断には、終了ステータスコードを利用すると思います。
そんな終了ステータスコードが、現状async functionだと想定外の動作になっており、やらかしそうになった話です。


# 読者対象

- Node.jsでローカル実行向けプログラムを作成し、コマンドをチェーン実行する方。

# lesson

## 前提

```npm start```を実行し、index.jsで例外を発生させ、badend.jsが実行されないことを確認していきます。

```json:package.json

{
  "name": "asyncexit",
  "description": "exit status of code",
  "scripts": {
    "start": "node index.js && node badend.js"
  },
  "dependencies": {},
  "private": true
}
```

```javascript:index.js

(() => {
	console.log("test");
    // main functionがステップごとに変更されます。
})();

```

```javascript:badend.js

(async () => {
    console.error("bad end...");
})();

```

## sync functionの場合（長いので自信のある方は飛ばしてください）
### index.jsが正常終了した場合

index.jsが正常終了した場合、badend.jsにつながることを確認しておきます。

```javascript:index.js
(() => {
	console.log("test");
})();

```

```terminal
PS C:\Users\z\src\asyncexit> npm start

> asyncexit@ start C:\Users\z\src\asyncexit
> node index.js && node badend.js

test
bad end...
PS C:\Users\z\src\asyncexit>
```

index.jsが正常終了すると、ターミナルにbad end...が表示され、想定外の実行になることが確認できました。
では、以降はindex.jsをエラーにして、badend.jsが実行されないことを確認していきます。

### exitCodeで異常終了

次は```process.exitCode```を0以外に設定することで、終了時に異常終了にします。

```javascript:index.js
(() => {
	console.log("test");
	process.exitCode = 1;
	console.log("test2");
})();

```

```errno 1```で終了し、badend.jsが実行されないことを確認できます。
なお、```process.exitCode```はそこで終了しないため、test2が出力されていることを確認しておきます。

```terminal
PS C:\Users\z\src\asyncexit> npm start

> asyncexit@ start C:\Users\z\src\asyncexit
> node index.js && node badend.js

test
test2
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! asyncexit@ start: `node index.js && node badend.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the asyncexit@ start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\z\AppData\Roaming\npm-cache\_logs\2019-04-28T12_16_17_583Z-debug.log
PS C:\Users\z\src\asyncexit>
```

### exit()で異常終了


次は```process.exit()```をコールすることで、異常終了にします。


```javascript:index.js
(() => {
	console.log("test");
	process.exit(1);
	console.log("test2");
})();

```

```errno 1```で終了し、badend.jsが実行されないことを確認できます。
なお、```process.exit()```はそこで終了するため、test2が出力されていないことも確認しておきます。

```terminal
PS C:\Users\z\src\asyncexit> npm start

> asyncexit@ start C:\Users\z\src\asyncexit
> node index.js && node badend.js

test
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! asyncexit@ start: `node index.js && node badend.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the asyncexit@ start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\z\AppData\Roaming\npm-cache\_logs\2019-04-28T12_21_53_525Z-debug.log
PS C:\Users\z\src\asyncexit>
```


### 例外で異常終了

次は例外発生によって異常終了するケースです。

```javascript:index.js
(() => {
    console.log("test");
    throw new Error("例外発生");
})();

```

基本的に```process.exit()```と同様に、終了ステータス1で異常終了となっています。
違いとしてはスタックトレースが出力されている程度でしょうか？

```terminal

PS C:\Users\z\src\asyncexit> npm start

> asyncexit@ start C:\Users\z\src\asyncexit
> node index.js && node badend.js

test
C:\Users\z\src\asyncexit\index.js:33
    throw new Error("例外発生");
    ^

Error: 例外発生
    at C:\Users\z\src\asyncexit\index.js:33:11
    at Object.<anonymous> (C:\Users\z\src\asyncexit\index.js:34:3)
    at Module._compile (internal/modules/cjs/loader.js:701:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
    at Module.load (internal/modules/cjs/loader.js:600:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
    at Function.Module._load (internal/modules/cjs/loader.js:531:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! asyncexit@ start: `node index.js && node badend.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the asyncexit@ start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\z\AppData\Roaming\npm-cache\_logs\2019-04-28T12_24_23_145Z-debug.log
PS C:\Users\z\src\asyncexit>
```

### 例外をリスローして異常終了

例外をキャッチし、握りつぶさずリスローすることで異常終了を確認します。

```javascript:index.js
(() => {
    try {
        console.log("test");
        throw new Error("例外発生");
    } catch (e) {
        console.error(e);
        throw e;
    }
})();
```

二重にスタックトレース出ていますが、問題なく異常終了しています。

```terminal
PS C:\Users\z\src\asyncexit> npm start

> asyncexit@ start C:\Users\z\src\asyncexit
> node index.js && node badend.js

test
Error: 例外発生
    at C:\Users\z\src\asyncexit\index.js:40:15
    at Object.<anonymous> (C:\Users\z\src\asyncexit\index.js:45:3)
    at Module._compile (internal/modules/cjs/loader.js:701:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
    at Module.load (internal/modules/cjs/loader.js:600:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
    at Function.Module._load (internal/modules/cjs/loader.js:531:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
C:\Users\z\src\asyncexit\index.js:43
        throw e;
        ^

Error: 例外発生
    at C:\Users\z\src\asyncexit\index.js:40:15
    at Object.<anonymous> (C:\Users\z\src\asyncexit\index.js:45:3)
    at Module._compile (internal/modules/cjs/loader.js:701:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
    at Module.load (internal/modules/cjs/loader.js:600:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
    at Function.Module._load (internal/modules/cjs/loader.js:531:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! asyncexit@ start: `node index.js && node badend.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the asyncexit@ start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\z\AppData\Roaming\npm-cache\_logs\2019-04-28T12_31_16_906Z-debug.log
PS C:\Users\z\src\asyncexit>

```



### 例外をキャッチしてproccess.exit()で終了

今度はリスローではなく、```proccess.exit()```で終了させてみます。

```javascript:index.js
(() => {
    try {
        console.log("test");
        throw new Error("例外発生");
    } catch (e) {
        console.error(e);
        process.exit(1);
    }
})();
```
例外をキャッチして、明示的に異常終了させれたことが確認できます。
補足としては、キャッチした場合はリスローなり```process.exit()```で異常終了させないとbadend.jsが実行されることに注意してください。

```terminal

PS C:\Users\z\src\asyncexit> npm start

> asyncexit@ start C:\Users\z\src\asyncexit
> node index.js && node badend.js

test
Error: 例外発生
    at C:\Users\z\src\asyncexit\index.js:51:15
    at Object.<anonymous> (C:\Users\z\src\asyncexit\index.js:56:3)
    at Module._compile (internal/modules/cjs/loader.js:701:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
    at Module.load (internal/modules/cjs/loader.js:600:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
    at Function.Module._load (internal/modules/cjs/loader.js:531:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! asyncexit@ start: `node index.js && node badend.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the asyncexit@ start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\z\AppData\Roaming\npm-cache\_logs\2019-04-28T12_36_30_826Z-debug.log
PS C:\Users\z\src\asyncexit>

```

sync functionに関して長々と載せましたが、いずれも想定内だったのではないでしょうか？
異常終了させる方法を押さえたうえで、メインディッシュのasync functionに着手します。

## async functionの場合

### async functionの正常終了パターン

ウォーミングアップは終了しましたので、以降はasync functionを使っていきます。
まずはindex.jsが正常終了するパターン(badend.jsが実行される)を確認しておきます。

```javascript:index.js

const resolvePromise = () => new Promise(resolve => resolve());

(async () => {
    await resolvePromise();
    console.log("test");
})();

```
resolvePromiseが正常終了していますので、badend.jsが実行されていることが確認できます。

```terminal
PS C:\Users\z\src\asyncexit> npm start

> asyncexit@ start C:\Users\z\src\asyncexit
> node index.js && node badend.js

test
bad end...
PS C:\Users\z\src\asyncexit>
```


### async functionで例外が発生したケース

asyncキーワード不要のsync functionですが、asyncキーワードを付けた場合、結果はどうなるでしょうか？

```javascript:index.js

(async () => {
	console.log("test");
	throw new Error("例外発生");
})();

```

**正解はbadend.jsが実行されてしまいます。**
sync functionでは例外が発生して異常終了するケースなのに・・・

```terminal
PS C:\Users\z\src\asyncexit> npm start

> asyncexit@ start C:\Users\z\src\asyncexit
> node index.js && node badend.js

test
(node:25332) UnhandledPromiseRejectionWarning: Error: 例外発生
    at C:\Users\z\src\asyncexit\index.js:77:8
    at Object.<anonymous> (C:\Users\z\src\asyncexit\index.js:79:3)
    at Module._compile (internal/modules/cjs/loader.js:701:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
    at Module.load (internal/modules/cjs/loader.js:600:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
    at Function.Module._load (internal/modules/cjs/loader.js:531:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
(node:25332) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:25332) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In
the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
bad end...
PS C:\Users\z\src\asyncexit>

```

この現象を理解するには、エラーメッセージを読む必要があります。
私の稚拙な翻訳よりGoogle翻訳先生のほうが精度高いので、Google翻訳先生のお力を借ります。

>UnhandledPromiseRejectionWarning：未処理の約束拒否。 このエラーは、catchブロックなしで非同期関数の内部をスローするか、または.catch（）で処理されなかった約束を拒否することによって発生しました。
DeprecationWarning：未処理の約束拒否は非推奨です。 将来的には、処理されない約束の拒否は、Node.jsプロセスをゼロ以外の終了コードで終了させるでしょう。

簡単に言うなら、async functionで例外をキャッチしないのはルール違反で、将来的に終了ステータスコードを0以外にするけど、今のところ0で終了するよ！

asyncがない場合には、例外をキャッチしなくても異常終了になっていましたが、**async functionの場合は終了ステータスコード上は正常終了扱いになっている**ということです。
これが私がやらかしそうになった例です。
async functionのjsをコピーして中身をちょっと書き換えたのですが、念のため異常ケースの確認をと思ったらこの現象に遭遇。
もし試してなかったら危うく本番データを不完全なデータで上書きするところでした:scream:


### 念のためreject()の場合はどうなるか

予想はつきますが、reject()の場合を確認しておきます。

```javascript:index.js
const rejectPromise = () => new Promise((resolve, reject) => reject());

(async () => {
    await rejectPromise();
    console.log("test");
})();
```

rejectの戻り値が未設定のため、undefinedの出力が増えていますが、基本的には前項と一緒ですね。

```terminal

PS C:\Users\z\src\asyncexit> npm start

> asyncexit@ start C:\Users\z\src\asyncexit
> node index.js && node badend.js

(node:8552) UnhandledPromiseRejectionWarning: undefined
(node:8552) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)
(node:8552) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with
a non-zero exit code.
bad end...
```

### async functionでrejectをキャッチする

キャッチしないのはルール違反ということなので、async functionでrejectをキャッチすることにします。

```javascript:index.js
const rejectPromise = () => new Promise((resolve, reject) => reject());

(async () => {
    try {
		await rejectPromise();
		console.log("test");
    } catch (e) {
        console.error(e);
        process.exit(1);
    }
})();

```

catchすることで、sync functionと同様の異常終了に持ち込めました。

```terminal
PS C:\Users\z\src\asyncexit> npm start

> asyncexit@ start C:\Users\z\src\asyncexit
> node index.js && node badend.js

undefined
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! asyncexit@ start: `node index.js && node badend.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the asyncexit@ start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\z\AppData\Roaming\npm-cache\_logs\2019-04-28T13_07_12_060Z-debug.log
PS C:\Users\z\src\asyncexit>
```

### async functionで例外をキャッチする

予想はつきますが、Promise内で例外発生の場合を確認しておきます。

```javascript:index.js
const throwPromise = () => new Promise(resolve => { throw new Error("例外発生"); resolve(); });

(async () => {
    try {
		await throwPromise();
		console.log("test");
    } catch (e) {
        console.error(e);
        process.exit(1);
    }
})();
```

例外のスタックトレースがある以外は、基本的には前項と一緒ですね。

```terminal
PS C:\Users\z\src\asyncexit> npm start

> asyncexit@ start C:\Users\z\src\asyncexit
> node index.js && node badend.js

Error: 例外発生
    at resolve (C:\Users\z\src\asyncexit\index.js:6:59)
    at new Promise (<anonymous>)
    at throwPromise (C:\Users\z\src\asyncexit\index.js:6:28)
    at C:\Users\z\src\asyncexit\index.js:156:9
    at Object.<anonymous> (C:\Users\z\src\asyncexit\index.js:162:3)
    at Module._compile (internal/modules/cjs/loader.js:701:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
    at Module.load (internal/modules/cjs/loader.js:600:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
    at Function.Module._load (internal/modules/cjs/loader.js:531:3)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! asyncexit@ start: `node index.js && node badend.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the asyncexit@ start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\z\AppData\Roaming\npm-cache\_logs\2019-04-28T13_26_01_558Z-debug.log
PS C:\Users\z\src\asyncexit>
```


# まとめ

asyncの思わぬ落とし穴ということで紹介しましたが、いかがでしたでしょうか？
思い込みほど危険なものはなく、大事なところはテストすることが大事と改めて認識した一幕でした。
将来的にはsync functionと同様になるとは思いますが、当面は終了ステータスコードを利用するなら、asyncにはcatchを同行させ、異常終了に導きましょう。
sync functionがasync functionをコールする変更に伴い、async functionに変化するケースは特に注意しましょう。