setIntervalの中でエラーが飛ぶとタイマーが停止する

サーバーとの定期的な疎通確認を `setInterval` で実装してたら、いつの間にか処理が停止してたという話。

setIntervalの中でエラーが飛ぶとタイマーが停止する。 ΩΩΩ

```js
process.on('uncaughtException', function (err) {
    console.error(err.stack);
});
 
setInterval(function () {
    JSON.parse('{}}}'); // throw
}, 1000);
 
setInterval(function () {
    console.log('xxx');
}, 1000);
```

実行結果。1秒ごとにエラーが発生すると思って書いたつもりだったけど発生しない。

```
$ node a.js
SyntaxError: Unexpected token }
    at Object.parse (native)
    at null.<anonymous> (/home/ajido/a.js:6:10)
    at wrapper [as _onTimeout] (timers.js:252:14)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)
xxx
xxx
xxx
xxx
xxx
xxx
xxx
```

スコープ内で `try..catch` するとタイマーは停止しないので、 `uncaughtException` でスコープ外に飛んだのが原因かと考え `domain` に変更してみるも結果は同じ。

```js
var d = require('domain').create();

d.on('error', function (err) {
    console.error(err.stack);
});

d.run(function () {
    setInterval(function () {
        throw new Error('hoge');
    }, 1000);

    setInterval(function () {
        console.log('xxx');
    }, 1000);
});
```

実行結果

```
Error: hoge
    at /home/ajido/a.js:10:19
    at b (domain.js:183:18)
    at Domain.run (domain.js:123:23)
    at null.<anonymous> (/home/ajido/a.js:9:11)
    at wrapper [as _onTimeout] (timers.js:252:14)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)
xxx
xxx
xxx
xxx
```

これならどうだと思ったけどこれも失敗。

```js:throwをdomainで直接包んでみる
ar d = require('domain').create();

d.on('error', function (err) {
    console.error(err.stack);
});

d.run(function () {
    setInterval(function () {
        d.run(function () {
            throw new Error('hoge');
        });
    }, 1000);

    setInterval(function () {
        d.run(function () {
            console.log('xxx');
        });
    }, 1000);
});
```

```js:domainごとsetInterval内に放り込む
setInterval(function () {
    var d = require('domain').create();

    d.on('error', function (err) {
        console.error(err.stack);
    });

    d.run(function () {
        throw new Error('hoge');
    });
}, 1000);

setInterval(function () {
    var d = require('domain').create();

    d.on('error', function (err) {
        console.error(err.stack);
    });

    d.run(function () {
        console.log('xxx');
    });
}, 1000);
```

実行結果

```
Error: hoge
    at /home/ajido/a.js:9:15
    at b (domain.js:183:18)
    at Domain.run (domain.js:123:23)
    at null.<anonymous> (/home/ajido/a.js:8:7)
    at wrapper [as _onTimeout] (timers.js:252:14)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)
xxx
xxx
xxx
xxx
```

### setTimeoutに変更して対処

`try..catch` ではすべてのエラーを担保できないので `setTimeout` の再起に変更して対処。ただエラー内容が `at Timer.listOnTimeout (timers.js:110:15)` になってうーんという感じ。

```js
process.on('uncaughtException', function (err) {
    console.error(err.stack);
});

function func() {
  setTimeout(func, 1000);
  console.log('xxx');
  throw new Error();
}

func();
```

ちなみにブラウザだとタイマーは停止しない。

```js
> setInterval(function (){ console.log ('xxx'); throw new Error(); }, 1000);
< 18
< xxx
< Uncaught Error
< xxx
< Uncaught Error
< xxx
< Uncaught Error
```

非同期処理の内部で発生する可能性のあるすべてのエラーをしっかりハンドリングしていれば、 `setInterval` 内で発生するエラーもフックできるので問題ないと思いますが、今回は定期的に行う疎通確認部分で外部のモジュールを使っていて、その中のソケット通信実装部分でエラーハンドリングに漏れがあったので上位の `uncaughtException/domain` まで飛んでいました。ちなみに `ECONNREFUSED` です。

ただこういうエラーハンドリングの漏れはよくあることだと思うので、気楽に `uncaughtException/domain` で引っかかったらそのときに修正すればいいやくらいの気分でいたんですが、まさかタイマーが停止するとは思ってなかった…

これ以上は追ってないのでどこかで関連する話がされていれば是非教えてくださいー