Repository: sindresorhus/execa (JavaScript)

Issue:
Output streams do not emit "end" event if execution fails
Consider this code (using node 18.13.0 and execa 6.1.0)
```js
import { execa } from 'execa';

const proc = execa( "does not exist", [], { stdio: [ 'ignore', 'pipe', 'inherit' ], buffer: false } );

proc.stdout.on( 'end', () => console.log( 'stdout ended' ) );
proc.stdout.on( 'close', () => console.log( 'stdout closed' ) );

await proc.catch( e => e );
```
Only "stdout closed" is logged, "stdout ended" never happens.

This does not match the behavior of `child_process`:
```js
import { spawn } from 'node:child_process';

const proc = spawn( "does not exist", [], { stdio: [ 'ignore', 'pipe', 'inherit' ] } );

proc.stdout.on( 'end', () => console.log( 'stdout ended' ) );
proc.stdout.on( 'close', () => console.log( 'stdout closed' ) );

proc.on( 'error', () => console.log( 'proc error' ) );
```
In this case both "stdout ended" and "stdout closed" fire as expected.


Discussion:
FYI, the code I'm trying to make work looks a bit more like this than the examples above:
```js
import { execa } from 'execa';
import readline from 'readline';

async function* listFiles( dir ) {
    const proc = execa( 'git', [ 'ls-files' ], {
        cwd: dir,
        stdio: [ 'ignore', 'pipe', null ],
        buffer: false
    } );

    const rl = readline.createInterface( {
        input: proc.stdout,
        crlfDelay: Infinity,
    } );

    // We can't allow proc to reject, see https://github.com/sindresorhus/execa/issues/516#issuecomment-1426154012
    // So catch the error and save it to rethrow later.
    let err;
    proc.catch( e => { err = e; } );

    // Yield each line of output.
    yield* rl;

    // If proc rejects, throw the error now.
    await proc;
    if ( err ) throw err;
}
```
The failure case is if `git` somehow does not exist in the path.

If you have any better ways to handle this code that don't result in reading the whole output of the command into memory, I'd be happy for the suggestion. The rearrangement suggested by @ehmicky in https://github.com/sindresorhus/execa/issues/516#issuecomment-1425018125 doesn't seem compatible with use of `yield`.
Hi @anomiex,

Good catch! I have opened #518 to fix this problem when `buffer` is `false`.
Released in [v7.0.0](https://github.com/sindresorhus/execa/releases/tag/v7.0.0)



Patch:
diff --git a/lib/stream.js b/lib/stream.js
index 3dcc81d07d..32bdefe9bc 100644
--- a/lib/stream.js
+++ b/lib/stream.js
@@ -36,7 +36,8 @@ export const makeAllStream = (spawned, {all}) => {
 
 // On failure, `result.stdout|stderr|all` should contain the currently buffered stream
 const getBufferedData = async (stream, streamPromise) => {
-	if (!stream) {
+	// When `buffer` is `false`, `streamPromise` is `undefined` and there is no buffered data to retrieve
+	if (!stream || streamPromise === undefined) {
 		return;
 	}
 


Test patch:
diff --git a/test/stream.js b/test/stream.js
index 32c3d0c678..aeceedb0d4 100644
--- a/test/stream.js
+++ b/test/stream.js
@@ -4,6 +4,7 @@ import fs from 'node:fs';
 import Stream from 'node:stream';
 import test from 'ava';
 import getStream from 'get-stream';
+import {pEvent} from 'p-event';
 import tempfile from 'tempfile';
 import {execa, execaSync} from '../index.js';
 import {setFixtureDir} from './helpers/fixtures-dir.js';
@@ -173,6 +174,11 @@ test('buffer: false > promise rejects when process returns non-zero', async t =>
 	t.is(exitCode, 2);
 });
 
+test('buffer: false > emits end event when promise is rejected', async t => {
+	const subprocess = execa('wrong command', {buffer: false, reject: false});
+	await t.notThrowsAsync(Promise.all([subprocess, pEvent(subprocess.stdout, 'end')]));
+});
+
 test('can use all: true with stdout: ignore', async t => {
 	await t.notThrowsAsync(execa('max-buffer.js', {buffer: false, stdout: 'ignore', all: true}));
 });