Repository: sindresorhus/execa (js)

Issue:
Include stdout/stderr in `error.message`
We removed it in `execa` v2 for some reason, but it's very useful.

The lack of it leads to a lot of annoyance and makes it harder for users to see what actually went wrong...

It also makes it harder to do test assertions like this:

```js
test('missing file operands', async t => {
	await t.throwsAsync(execa('./cli.js'), /`source` and `destination` required/);
});
```

Open questions:
- Should we include `stdout` or only `stderr`?
- Should we truncate it at some length?

Pull request description:
Add stdout/stderr to error messages
Fixes #395.

This adds stderr and stdout (in that order) to error messages. 

stdout and stderr are not added when they are not available, for example when they are streamed (`stdio: 'inherit'`).

A new property `error.shortMessage` is added to retrieve error messages without stdout/stderr in case users need shorter error messages.

This PR includes tests, documentation, TypeScript types.

Examples for `error.errno` were also adjusted to match Node 13 output.

Patch:
diff --git a/index.d.ts b/index.d.ts
index 83a9953..76a20bf 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -44,7 +44,7 @@ declare namespace execa {
 		This can be either an absolute path or a path relative to the `cwd` option.
 
 		Requires `preferLocal` to be `true`.
-		
+
 		For example, this can be used together with [`get-node`](https://github.com/ehmicky/get-node) to run a specific Node.js version in a child process.
 
 		@default process.execPath
@@ -336,14 +336,21 @@ declare namespace execa {
 		extends Error,
 			ExecaReturnBase<StdoutErrorType> {
 		/**
-		The error message.
+		Error message when the child process failed to run. In addition to the underlying error message, it also contains some information related to why the child process errored.
+
+		The child process stderr then stdout are appended to the end, separated with newlines and not interleaved.
 		*/
 		message: string;
 
 		/**
-		Original error message. This is `undefined` unless the child process exited due to an `error` event or a timeout.
+		This is the same as the `message` property except it does not include the child process stdout/stderr.
+		*/
+		shortMessage: string;
+
+		/**
+		Original error message. This is the same as the `message` property except it includes neither the child process stdout/stderr nor some additional information added by Execa.
 
-		The `message` property contains both the `originalMessage` and some additional information added by Execa.
+		This is `undefined` unless the child process exited due to an `error` event or a timeout.
 		*/
 		originalMessage?: string;
 	}
diff --git a/lib/error.js b/lib/error.js
index 808ad43..ea04b90 100644
--- a/lib/error.js
+++ b/lib/error.js
@@ -47,15 +47,18 @@ const makeError = ({
 	const errorCode = error && error.code;
 
 	const prefix = getErrorPrefix({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled});
-	const message = `Command ${prefix}: ${command}`;
+	const execaMessage = `Command ${prefix}: ${command}`;
+	const shortMessage = error instanceof Error ? `${execaMessage}\n${error.message}` : execaMessage;
+	const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n');
 
 	if (error instanceof Error) {
 		error.originalMessage = error.message;
-		error.message = `${message}\n${error.message}`;
+		error.message = message;
 	} else {
 		error = new Error(message);
 	}
 
+	error.shortMessage = shortMessage;
 	error.command = command;
 	error.exitCode = exitCode;
 	error.signal = signal;
diff --git a/lib/kill.js b/lib/kill.js
index dc1c77c..813cf2e 100644
--- a/lib/kill.js
+++ b/lib/kill.js
@@ -18,9 +18,14 @@ const setKillTimeout = (kill, signal, options, killResult) => {
 	}
 
 	const timeout = getForceKillAfterTimeout(options);
-	setTimeout(() => {
+	const t = setTimeout(() => {
 		kill('SIGKILL');
-	}, timeout).unref();
+	}, timeout);
+
+	// istanbul ignore else
+	if (t.unref) {
+		t.unref();
+	}
 };
 
 const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => {
diff --git a/readme.md b/readme.md
index 0f01d61..68d5986 100644
--- a/readme.md
+++ b/readme.md
@@ -60,12 +60,13 @@ const execa = require('execa');
 		/*
 		{
 			message: 'Command failed with ENOENT: unknown command spawn unknown ENOENT',
-			errno: 'ENOENT',
+			errno: -2,
 			code: 'ENOENT',
 			syscall: 'spawn unknown',
 			path: 'unknown',
 			spawnargs: ['command'],
 			originalMessage: 'spawn unknown ENOENT',
+			shortMessage: 'Command failed with ENOENT: unknown command spawn unknown ENOENT',
 			command: 'unknown command',
 			stdout: '',
 			stderr: '',
@@ -112,12 +113,13 @@ try {
 	/*
 	{
 		message: 'Command failed with ENOENT: unknown command spawnSync unknown ENOENT',
-		errno: 'ENOENT',
+		errno: -2,
 		code: 'ENOENT',
 		syscall: 'spawnSync unknown',
 		path: 'unknown',
 		spawnargs: ['command'],
 		originalMessage: 'spawnSync unknown ENOENT',
+		shortMessage: 'Command failed with ENOENT: unknown command spawnSync unknown ENOENT',
 		command: 'unknown command',
 		stdout: '',
 		stderr: '',
@@ -302,13 +304,27 @@ A human-friendly description of the signal that was used to terminate the proces
 
 If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. It is also `undefined` when the signal is very uncommon which should seldomly happen.
 
+#### message
+
+Type: `string`
+
+Error message when the child process failed to run. In addition to the [underlying error message](#originalMessage), it also contains some information related to why the child process errored.
+
+The child process [stderr](#stderr) then [stdout](#stdout) are appended to the end, separated with newlines and not interleaved.
+
+#### shortMessage
+
+Type: `string`
+
+This is the same as the [`message` property](#message) except it does not include the child process stdout/stderr.
+
 #### originalMessage
 
 Type: `string | undefined`
 
-Original error message. This is `undefined` unless the child process exited due to an `error` event or a timeout.
+Original error message. This is the same as the `message` property except it includes neither the child process stdout/stderr nor some additional information added by Execa.
 
-The `message` property contains both the `originalMessage` and some additional information added by Execa.
+This is `undefined` unless the child process exited due to an `error` event or a timeout.
 
 ### options
 


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index c9845d1..450cd40 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -40,6 +40,7 @@ try {
 	expectType<boolean>(execaError.killed);
 	expectType<string | undefined>(execaError.signal);
 	expectType<string | undefined>(execaError.signalDescription);
+	expectType<string>(execaError.shortMessage);
 	expectType<string | undefined>(execaError.originalMessage);
 }
 
@@ -70,6 +71,7 @@ try {
 	expectType<boolean>(execaError.killed);
 	expectType<string | undefined>(execaError.signal);
 	expectType<string | undefined>(execaError.signalDescription);
+	expectType<string>(execaError.shortMessage);
 	expectType<string | undefined>(execaError.originalMessage);
 }
 
diff --git a/test/error.js b/test/error.js
index 7a79312..386c650 100644
--- a/test/error.js
+++ b/test/error.js
@@ -51,7 +51,25 @@ test('exitCode is 3', testExitCode, 3);
 test('exitCode is 4', testExitCode, 4);
 
 test('error.message contains the command', async t => {
-	await t.throwsAsync(execa('exit', ['2', 'foo', 'bar'], {message: /exit 2 foo bar/}));
+	await t.throwsAsync(execa('exit', ['2', 'foo', 'bar']), {message: /exit 2 foo bar/});
+});
+
+test('error.message contains stdout/stderr if available', async t => {
+	const {message} = await t.throwsAsync(execa('echo-fail'));
+	t.true(message.includes('stderr'));
+	t.true(message.includes('stdout'));
+});
+
+test('error.message does not contain stdout/stderr if not available', async t => {
+	const {message} = await t.throwsAsync(execa('echo-fail', {stdio: 'ignore'}));
+	t.false(message.includes('stderr'));
+	t.false(message.includes('stdout'));
+});
+
+test('error.shortMessage does not contain stdout/stderr', async t => {
+	const {shortMessage} = await t.throwsAsync(execa('echo-fail'));
+	t.false(shortMessage.includes('stderr'));
+	t.false(shortMessage.includes('stdout'));
 });
 
 test('Original error.message is kept', async t => {
diff --git a/test/fixtures/echo-fail b/test/fixtures/echo-fail
new file mode 100755
index 0000000..233f1d0
--- /dev/null
+++ b/test/fixtures/echo-fail
@@ -0,0 +1,5 @@
+#!/usr/bin/env node
+'use strict';
+console.log('stdout');
+console.error('stderr');
+process.exit(1);