Node.jsの「ERR_STREAM_DESTROYED: Cannot call write after a stream was destroyed」を修正する

intermediate💚 Node.js2026-05-10| Node.js 10以降、全プラットフォーム(Linux、macOS、Windows)

Error Message

Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed
#nodejs#ストリーム#書き込みストリーム#パイプ#破棄

エラー

Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed

すでに閉じられたストリームに書き込もうとしました。クライアントがレスポンスの途中で切断したか、非同期のデータベース呼び出しがres.end()の実行後200ms遅れて返ってきた可能性があります。Node.jsは内部のdestroyedフラグを設定し、それ以降の書き込みをすべて拒否して、未処理の例外またはストリームのerrorイベントとしてこのエラーをスローします。

根本原因

Node.jsのストリームには厳密なライフサイクルがあります。stream.destroy()が呼び出されると——コードで明示的に、またはHTTPレスポンスの終了やソケットの切断時に自動的に——destroyedフラグがtrueに切り替わります。これは永続的な状態です。それ以降の.write()呼び出しはすべてこのエラーをスローします。

よくある原因:

  • res.end()の呼び出し後、またはクライアントの切断後にHTTPレスポンスへ書き込む
  • 非同期コールバック(データベースクエリ、外部API呼び出し)がストリームのクローズ後に解決される
  • パイプされた読み取りストリームが、書き込み先の破棄後にデータをプッシュする
  • 破棄済みのストリームオブジェクトを再利用する

修正方法1 — 書き込み前にdestroyedを確認する

最もシンプルな修正方法:書き込む前にフラグを確認します。

function safeWrite(stream, chunk) {
  if (stream.destroyed) {
    return; // silently skip — stream is gone
  }
  stream.write(chunk);
}

HTTPレスポンスの場合、確認すべきフラグが2つあります:

app.get('/data', async (req, res) => {
  const data = await fetchSomething();

  if (res.destroyed || res.writableEnded) {
    return; // client disconnected or res.end() already called
  }

  res.json(data);
});

res.writableEndedres.end()が呼び出された瞬間にtrueになります。res.destroyedは基盤となるソケットが失われた時点でtrueになります。どちらの場合も致命的ですので、両方を確認してください。

修正方法2 — closeイベントで非同期処理をキャンセルする

データベースから行をストリーミングするルートがありますか?5,000行のうち50行を受信した後にクライアントが切断すると、ループが書き込みを続けることでこのエラーが発生します。closeイベントでキャンセルフラグを設定し、早期に処理を中断します。

app.get('/stream-data', (req, res) => {
  let cancelled = false;

  res.on('close', () => {
    cancelled = true; // client disconnected
  });

  async function streamRows() {
    for await (const row of getDatabaseRows()) {
      if (cancelled) break;
      res.write(JSON.stringify(row) + '\n');
    }
    if (!cancelled) res.end();
  }

  streamRows().catch(err => {
    if (!cancelled) res.destroy(err);
  });
});

修正方法3 — パイプラインでAbortControllerを使用する

ストリームをパイプしていますか?stream.pipeline()AbortSignalを渡すことで、切断時にチェーン全体をクリーンに中断できます——不要な書き込みの試みが発生しません。

const { pipeline } = require('stream/promises');
// AbortController is built-in from Node.js 16+

const ac = new AbortController();

req.on('close', () => ac.abort()); // client disconnected → abort

try {
  await pipeline(sourceStream, transformStream, res, { signal: ac.signal });
} catch (err) {
  if (err.name !== 'AbortError') {
    console.error('Pipeline failed:', err);
  }
}

stream.pipeline()はエラーまたは中断時にチェーン内のすべてのストリームを破棄します。手動のクリーンアップは不要です。

修正方法4 — ストリームのerrorイベントをキャッチする

すべての書き込みパスを保護できない場合もあります。最低限、エラーハンドラをアタッチして、エラーがプロセス全体をクラッシュさせないようにします。

const writable = getWritableStream();

writable.on('error', (err) => {
  if (err.code === 'ERR_STREAM_DESTROYED') {
    // expected — stream closed before all data was written
    return;
  }
  console.error('Unexpected stream error:', err);
});

これはセーフティネットです。エラーの発生を防ぐわけではなく、サーバーをダウンさせないようにするだけです。

修正方法5 — 書き込み中のストリームを破棄しない

ストリームのクローズタイミングを制御できる場合は、先にドレインさせてください。

// Wrong — destroys immediately, pending writes fail
stream.write(largeChunk);
stream.destroy();

// Correct — end() flushes pending writes then closes
stream.write(largeChunk);
stream.end(); // waits for write to complete, then closes gracefully

クリーンなシャットダウンにはstream.end()を使用します。stream.destroy()は、大容量アップロードの失敗時など、フラッシュせずに強制クローズが必要なエラーパスのために残しておいてください。

確認方法

修正後、問題が解消されたことを確認するために失敗を再現します:

  • サーバーを起動してストリーミングエンドポイントにアクセスします。レスポンスの途中で切断します——ブラウザでfetchをキャンセルするか、curlを実行してストリーミング中にCtrl+Cを押します
  • ログを確認します。ERR_STREAM_DESTROYEDが表示されないはずです
  • CIの場合、ストリームをパイプラインの途中で破棄し、未処理エラーが発生しないことを検証するテストを追加します:
it('does not throw when stream is destroyed mid-write', (done) => {
  const writable = new PassThrough();
  writable.on('error', done); // fail test on unexpected error

  writable.write('first chunk');
  writable.destroy();

  // safeWrite should swallow ERR_STREAM_DESTROYED silently
  expect(() => safeWrite(writable, 'second chunk')).not.toThrow();
  done();
});

予防策

  • 手動の書き込みループよりstream.pipeline()または.pipe()を優先する——後処理が自動化される
  • HTTPレスポンスのcloseイベントをリッスンして、クライアントの切断を早期に検知する
  • 非同期書き込みの前に必ずstream.destroyedまたはres.writableEndedを確認する
  • まずstream.end()を使用し、フラッシュなしの強制クローズが必要な場合のみstream.destroy()を使用する
  • Node.js 16以降では、キャンセル可能なパイプラインのためにAbortControllerstream.pipeline()に接続する

Related Error Notes