0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Windows の Node で fetch のあとに process.exit(0) すると、仕事を終えたあとに終了コード 3221226505 で落ちる回があった。このPCで数えると fetch 3回と4回で20回中20回、5回で1〜5回、8回でも合わせて220回中1回だった

0
Last updated at Posted at 2026-10-14

定期実行している確認用のスクリプトが、表示も処理もすべて正しく終わったあとに、失敗の終了コードを返していました。

Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 94

最後の1行が process.exit(0) で、その前に同じサーバへ fetch を何回か投げていました。この行は、30分ごとに走らせている稼働確認のログに2回残っています(2026-08-17 と 2026-08-19)。2回とも、確認の結果を表示し終えたあとに出ています。

今回、何回 fetch したときに落ちるのかを、依存のない再現スクリプトで数え直しました。

測った形

127.0.0.1 に使い捨てのサーバを立て、そこへ fetch を N 回投げて本文を読み切り、done と出力してから終わります。終わり方は2通りです。

  • process.exit(0) で終わる
  • process.exitCode = 0 を置き、サーバを閉じて、関数が終わるのを待つ

それぞれを子プロセスとして20回ずつ起動し、子の終了コードを数えました。このスクリプトを5回流しています。測ったのは Windows 11 / Node v24.18.1 の1台だけです。

結果

1回目に流した結果です。

fetch の回数 process.exit(0) process.exitCode = 0
0 0 が 20回 0 が 20回
1 0 が 20回 0 が 20回
2 0 が 20回 0 が 20回
3 3221226505 が 20回 0 が 20回
4 3221226505 が 20回 0 が 20回
5 0 が 18回 / 3221226505 が 2回 0 が 20回
8 0 が 20回 0 が 20回
  • 落ちた回も含めて、280回すべてで子は done を出力していました。仕事は終わっています。落ちているのは、そのあとです
  • 再現スクリプトは、欄ごとに最初に落ちた1回の標準エラーだけを残しています。残した3行は、どれも上の Assertion failed の行でした(他の落ちた回の標準エラーは見ていません)
  • process.exitCode = 0 で終わる形は、7通りの回数で合わせて140回、すべて 0 でした
  • **5回の欄だけは、流すたびに変わりました。**同じスクリプトを5回流すと、落ちた回は 2, 2, 5, 4, 1 回(各20回中)でした。残りの13欄は5回とも上の表と同じ数でした

**回数が増えるほど落ちる、という形ではありませんでした。**3回と4回は毎回落ち、5回はたまに落ち、8回は1度も落ちていません。なぜこの回数で落ちるのかは確かめていません。このPCとこの版でこう出た、というところまでです。

3221226505 は16進で 0xC0000409 です。符号付きで読むと -1073740791 になります。

シェルによって見え方が違う

同じ落ち方(fetch 3回 → process.exit(0))を、3つのシェルから起動して終了コードを表示させました。対照として、正常に終わる形(process.exitCode = 0)も同じ経路で起動しています。

シェル process.exit(0)(落ちる形) process.exitCode = 0(対照)
Git Bash の $? 127 0
PowerShell の $LASTEXITCODE -1073740791 0
cmd の %ERRORLEVEL% -1073740791 0

Git Bash では 127 と出ました。127 は bash では『コマンドが見つからない』ときにも出る値です。対照の形で 0 が出ているので、同じ経路で node は見つかっています。なぜ 127 になるのかは確かめていません。

定期実行で何が起きるか

タスク スケジューラは、起動したものの終了コードを前回の結果として記録します。起動したものが node の終了コードをそのまま返す作りなら、仕事は全部済んでいるのに、失敗の終了コードが記録されることになります。(タスク スケジューラ経由では今回は測っていません。間に挟む .cmd が最後に exit /b 0 していれば、記録は 0 になります)

これは成功を失敗として返す形です。毎回出るなら気づけますが、上の表のとおり回数によっては出たり出なかったりするので、fetch の回数が変わる処理(未読があるときだけもう1回問い合わせる、など)では、ある日から失敗が記録されはじめる、という見え方になりえます。

直し方

process.exit() を呼ばず、process.exitCode に終了コードを置いて、自分で開いたものを閉じて関数が終わるのを待ちます。

// 落ちる回があった形
process.exit(0);

// 替えた形
process.exitCode = 0;
server.closeAllConnections(); // 自分で開いたものは自分で閉じる
server.close();

上の表の右の列が、この形で測ったものです。

**閉じ忘れると、今度は終わりません。**上の再現の子から閉じる2行だけを消して process.exitCode = 0 で終わらせると、done を出したあとプロセスが終わらず、15秒の打ち切りで止められました(1回だけ確かめました)。定期実行なら、タスクの実行時間の上限まで居座ることになります。

AbortSignal.timeout() だけでは説明できなかった

原因の候補として AbortSignal.timeout() の未発火のタイマーを疑い、コードのコメントにそう書いたことがあります。今回、自分のサーバ(別のプロセスで動いているもの)相手に fetch を2回投げて process.exit(0) する形で、signal を付けない / AbortSignal.timeout() を付ける / 自前の AbortController で付けて必ず clearTimeout する、の3通りを10回ずつ試すと、3通りとも10回中10回落ちました。AbortSignal.timeout() を外しても直らないので、少なくともそれだけが原因ではありません。今日の測り方で言えるのは、process.exit(0) をやめれば出なかった、までです。

その測り方では2回で落ち、上の再現スクリプトでは2回では落ちず3回で落ちています。2つの測り方は、サーバが別のプロセスか同じプロセスか・相手のパスと応答・終わるときにサーバを開いたままか、など何か所も違うので、どれが境目を動かしたのかは分かりません。言えるのは、測り方が違うと境目が変わった、までです。

測っていないこと

  • 測ったのは Windows 11 / Node v24.18.1 の1台だけです。他の版の Node、Linux や macOS では測っていません
  • 相手は 127.0.0.1 の使い捨てサーバだけです。外のサーバ相手では測っていません
  • 再現の表は各欄20回です。0回と書いた欄も、21回目に落ちないとは言えません
  • シェルの表は各欄1回、signal を変えた測り直しは各10回です
  • なぜこの回数で落ちるのか、なぜ Git Bash で 127 になるのかは確かめていません

再現スクリプト

依存はありません。ファイルの読み書きはせず、通信は 127.0.0.1 の使い捨てサーバだけです。全部で 280回 の子プロセスを起動します。

// fetch のあとに process.exit(0) で終わると、Windows の Node が
// 仕事を終えたあとに終了コード 3221226505(= -1073740791)で落ちることがあるかを数える。
// 依存0。ファイルを読み書きしない。通信は 127.0.0.1 の使い捨てサーバだけ。
//
// 使い方: node repro-exit-after-fetch.mjs [各条件の回数(既定20)] [--json]
//
// 子プロセスとして自分自身を呼び、fetch の回数と終わり方(process.exit(0) / process.exitCode = 0)
// を変えて、子の終了コードを数える。
import http from "node:http";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";

const self = fileURLToPath(import.meta.url);

if (process.argv[2] === "--child") {
  const n = Number(process.argv[3]);
  const end = process.argv[4];
  const srv = http.createServer((req, res) => res.end("ok"));
  await new Promise((r) => srv.listen(0, "127.0.0.1", r));
  const url = `http://127.0.0.1:${srv.address().port}/`;
  for (let i = 0; i < n; i++) {
    const r = await fetch(url);
    await r.text();
  }
  console.log("done");
  if (end === "exit") process.exit(0);
  process.exitCode = 0;
  srv.closeAllConnections();
  srv.close();
} else {
  const runs = Number(process.argv.find((a) => /^\d+$/.test(a)) || 20);
  const rows = [];
  for (const n of [0, 1, 2, 3, 4, 5, 8]) {
    for (const end of ["exit", "exitCode"]) {
      const codes = {};
      let printedDone = 0;
      let firstStderr = null;
      for (let i = 0; i < runs; i++) {
        const r = spawnSync(process.execPath, [self, "--child", String(n), end], {
          encoding: "utf8",
          timeout: 20000,
        });
        const k = String(r.status);
        codes[k] = (codes[k] || 0) + 1;
        if (r.stdout.includes("done")) printedDone++;
        if (r.status !== 0 && firstStderr === null) firstStderr = r.stderr.trim().split(/\r?\n/)[0] || "";
      }
      rows.push({ fetches: n, end, runs, codes, printedDone, firstStderr });
    }
  }
  const out = { node: process.version, platform: process.platform, arch: process.arch, rows };
  if (process.argv.includes("--json")) {
    console.log(JSON.stringify(out, null, 2));
  } else {
    console.log(`Node ${out.node} / ${out.platform} ${out.arch} / 各条件 ${runs} 回`);
    for (const r of rows) {
      const c = Object.entries(r.codes).map(([k, v]) => `${k}:${v}`).join(" ");
      console.log(`fetch ${r.fetches}回  ${r.end.padEnd(8)}  終了コード ${c}  (done を出した回 ${r.printedDone})`);
    }
  }
}

Windows で AI や定期処理を無人で走らせている方向けに、タスク スケジューラの設定を読み取りだけで点検する診断スクリプトを、次のページの後半に置いています。

追記(2026-10-19): fetch 8回の欄も、落ちる回がありました

題と本文で『8回で0回』『8回は1度も落ちていません』と書きました。これは 2026-10-15 に流した5回(各20回)についての話で、8回なら落ちないという意味では成り立っていませんでした。

2026-10-18 に同じ再現スクリプトを流し直したところ、**fetch 8回 → process.exit(0) の欄が、20回中1回 3221226505 で終わりました。**残した標準エラーは、本文と同じ Assertion failed: !(handle->flags & UV_HANDLE_CLOSING) の行でした。続けて同じスクリプトを5回流すと、この欄は5回とも0回でした。

流した日 流した回数(各20回) fetch 8回 → process.exit(0) で落ちた回
2026-10-15 5回 0, 0, 0, 0, 0
2026-10-18 6回 1, 0, 0, 0, 0, 0

合わせて 220回中1回です。落ちる頻度は5回の欄(保存してある10回では毎回1〜5回)よりずっと低いものの、8回の欄も流すたびに変わりうる、と読むのが正確でした。3回と4回は、2026-10-18 の6回でも毎回20回中20回落ちました。process.exitCode = 0 で終わる形は、2026-10-18 の6回でも1度も落ちていません。

本文の結論(process.exit(0) をやめて process.exitCode を使い、自分で開いたものを閉じる)は変わりません。変わるのは『8回なら安全』と読める書き方で、そうは言えませんでした。

測っていないこと: このPC1台だけです。版を記録してある回はどれも Node v24.18.1 でした(2026-10-18 の1回目は版を記録していません)。8回の欄が落ちる割合は、1回しか観測していないので数として出せません。なぜその回だけ落ちたのかも確かめていません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?