ReadableStreamDefaultReader.read() Promise拒绝后,何时可安全再次调用?
Great question! You're spot-on about the common case: when reader.read() rejects (like with a fetch network failure), the underlying stream almost always enters an errored state. Any subsequent calls to read() will just re-throw the same error, so continuing to read is unsafe and pointless.
But there are edge cases—specifically with custom ReadableStream implementations—where you can recover and keep reading after a rejection. Let's break them down:
Custom streams with recoverable pull errors
If you've built your ownReadableStreamusing anunderlyingSource, you can design thepull()method to handle temporary errors without marking the entire stream as errored. Here's the key: don't callcontroller.error()for errors you want to recover from. Instead, handle the error internally and let the nextpull()attempt proceed.For example, imagine a stream that fetches chunks from a flaky API:
const flakyStream = new ReadableStream({ async pull(controller) { try { const response = await fetch('/flaky-api/chunk'); const chunk = await response.text(); controller.enqueue(chunk); } catch (err) { // Only mark the stream as errored for permanent failures if (err.status === 500 || err.type === 'PERMANENT') { controller.error(err); return; } // For temporary issues (e.g., 503, network blip), just skip this pull console.log('Temporary failure, will retry on next read'); } } }); const reader = flakyStream.getReader(); async function readLoop() { while (true) { try { const { done, value } = await reader.read(); if (done) break; processChunk(value); } catch (err) { // Check if the error is recoverable if (err.status === 503 || err.type === 'TEMPORARY') { console.log('Recoverable error, continuing read loop'); continue; } // Fatal error: exit the loop console.error('Unrecoverable stream error:', err); break; } } }In this case, a temporary failure in
pull()causes the currentreader.read()to reject, but the stream itself stays in a valid state. The nextread()call will trigger a newpull()attempt, allowing you to keep reading.Releasing the lock and re-acquiring a reader (for restartable streams)
If your custom stream is designed to be restartable (e.g., it can reset its internal state after an error), you can release the lock on the errored reader and get a new one. This only works if the stream hasn't been permanently closed or errored by the underlying source.For example:
async function restartRead(stream) { let reader = stream.getReader(); try { const { done, value } = await reader.read(); // Process value... } catch (err) { // If recoverable, release the lock and get a new reader if (err.type === 'RESTARTABLE') { reader.releaseLock(); reader = stream.getReader(); // Continue reading with the new reader await restartRead(stream); } } }Note that this is a niche use case—most streams (including all native browser streams like fetch responses) don't support restarting after an error.
Critical Caveat for Native Streams
It's important to emphasize: none of these recovery scenarios apply to native streams like fetch responses or FileReader streams. These streams are one-time, immutable, and any error (like a network failure) puts the stream into a permanent errored state. Any subsequent read() calls will immediately reject with the same error, so you must stop reading entirely.
In short: recovery after a reader.read() rejection is only possible if you've explicitly designed your custom ReadableStream to support it. For all out-of-the-box streams, a rejection means the stream is dead, and you should clean up immediately.
内容的提问来源于stack exchange,提问作者thurt

