Repository: sindresorhus/execa (JavaScript)

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?


Discussion:
Following from some discussion with @sindresorhus, we thought of the following solution:
  - `error.message` should include prefixes (e.g. `Command failed with exit code`) and be appended with `stdout` and `stderr` (if they exist)
  - `error.shortMessage` should include prefixes but not `stdout`/`stderr`
  - `error.originalMessage` should include neither prefixes nor `stdout`/`stderr`

This would be a breaking change.



Patch:
diff --git a/index.d.ts b/index.d.ts
index 83a995301e..76a20bfb38 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 808ad4384b..ea04b909f0 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/readme.md b/readme.md
index 0f01d61e63..68d59863ae 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 c9845d12a5..450cd40ff1 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 7a79312c52..386c6508c3 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 0000000000..233f1d0c07
--- /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);