Repository: sindresorhus/execa (js)

Issue:
Inconsistency between `result.stdout` and `error.stdout`.
We've been trying to keep the shape of an Execa's successful `result` and an `error` as close to each other as possible.

At the moment, `result.stdout` is:
  - `undefined` when: 
     - `childProcess.stdout` is `undefined`, e.g. when using `stdout: 'ignore'`
     - `buffer` is `false`
  - an empty string when there is an `stdout` but it is empty

Distinguishing between those two is useful. However, `error.stdout` does not do this: instead of `undefined`, it is an empty string.
We should fix this and make `error.stdout` behave like `result.stdout`.

This also applies to `result.stderr` and `result.all`.

We should also fix its types as suggested in #673.

Pull request description:
Fix inconsistency between `result.stdout` and `error.stdout`
Fixes #674.

Patch:
diff --git a/index.d.ts b/index.d.ts
index 0a3aec1..f942ad4 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -83,7 +83,7 @@ type TupleItem<
 // Whether `result.stdout|stderr|all` is `undefined`, excluding the `buffer` option
 type IgnoresStreamResult<
 	StreamIndex extends string,
-	OptionsType extends Options = Options,
+	OptionsType extends CommonOptions = CommonOptions,
 	// `result.stdin` is always `undefined`
 > = StreamIndex extends '0' ? true
 	// When using `stdout|stderr: 'inherit'`, or `'ignore'`, etc. , `result.std*` is `undefined`
@@ -114,19 +114,19 @@ type IgnoresStdioResult<
 // Whether `result.stdout|stderr|all` is `undefined`
 type IgnoresStreamOutput<
 	StreamIndex extends string,
-	OptionsType extends Options = Options,
+	OptionsType extends CommonOptions = CommonOptions,
 > = OptionsType extends {buffer: false} ? true
 	: IgnoresStreamResult<StreamIndex, OptionsType> extends true ? true : false;
 
 // Type of `result.stdout|stderr`
 type StdioOutput<
 	StreamIndex extends string,
-	OptionsType extends Options = Options,
+	OptionsType extends CommonOptions = CommonOptions,
 > = IgnoresStreamOutput<StreamIndex, OptionsType> extends true
 	? undefined
 	: StreamResult<OptionsType>;
 
-type StreamResult<OptionsType extends Options = Options> =
+type StreamResult<OptionsType extends CommonOptions = CommonOptions> =
 	// Default value for `encoding`, when `OptionsType` is `{}`
 	unknown extends OptionsType['encoding'] ? string
 		// Any value for `encoding`, when `OptionsType` is `Options`
@@ -142,14 +142,14 @@ type AllOutput<OptionsType extends Options = Options> = IgnoresStreamOutput<'1',
 	: StdioOutput<'1', OptionsType>;
 
 // Type of `result.stdio`
-type StdioArrayOutput<OptionsType extends Options = Options> = MapStdioOptions<
+type StdioArrayOutput<OptionsType extends CommonOptions = CommonOptions> = MapStdioOptions<
 OptionsType['stdio'] extends StdioOptionsArray ? OptionsType['stdio'] : ['pipe', 'pipe', 'pipe'],
 OptionsType
 >;
 
 type MapStdioOptions<
 	StdioOptionsArrayType extends StdioOptionsArray,
-	OptionsType extends Options = Options,
+	OptionsType extends CommonOptions = CommonOptions,
 > = {
 	[StreamIndex in keyof StdioOptionsArrayType]: StdioOutput<
 	StreamIndex extends string ? StreamIndex : string,
@@ -157,7 +157,12 @@ type MapStdioOptions<
 	>
 };
 
-export type Options<IsSync extends boolean = boolean> = {
+type StricterOptions<
+	WideOptions extends CommonOptions,
+	StrictOptions extends CommonOptions,
+> = keyof WideOptions extends never ? {} : WideOptions & StrictOptions;
+
+type CommonOptions<IsSync extends boolean = boolean> = {
 	/**
 	Prefer locally installed binaries when looking for a binary to execute.
 
@@ -454,7 +459,8 @@ export type Options<IsSync extends boolean = boolean> = {
 	readonly signal?: AbortSignal;
 });
 
-export type SyncOptions = Options<true>;
+export type Options = CommonOptions<false>;
+export type SyncOptions = CommonOptions<true>;
 
 export type NodeOptions<OptionsType extends Options = Options> = {
 	/**
@@ -482,7 +488,7 @@ The child process fails when:
 - being canceled
 - there's not enough memory or there are already too many child processes
 */
-export type ExecaReturnValue<IsSync extends boolean = boolean, OptionsType extends Options = Options> = {
+type ExecaCommonReturnValue<IsSync extends boolean = boolean, OptionsType extends CommonOptions = CommonOptions> = {
 	/**
 	The file and arguments that were run, for logging purposes.
 
@@ -581,9 +587,10 @@ export type ExecaReturnValue<IsSync extends boolean = boolean, OptionsType exten
 	all?: AllOutput<OptionsType>;
 });
 
-export type ExecaSyncReturnValue<OptionsType extends Options = Options> = ExecaReturnValue<true, OptionsType>;
+export type ExecaReturnValue<OptionsType extends Options = Options> = ExecaCommonReturnValue<false, OptionsType>;
+export type ExecaSyncReturnValue<OptionsType extends SyncOptions = SyncOptions> = ExecaCommonReturnValue<true, OptionsType>;
 
-export type ExecaError<IsSync extends boolean = boolean, OptionsType extends Options = Options> = {
+type ExecaCommonError = {
 	/**
 	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.
 
@@ -602,9 +609,10 @@ export type ExecaError<IsSync extends boolean = boolean, OptionsType extends Opt
 	This is `undefined` unless the child process exited due to an `error` event or a timeout.
 	*/
 	originalMessage?: string;
-} & Error & ExecaReturnValue<IsSync, OptionsType>;
+} & Error;
 
-export type ExecaSyncError<OptionsType extends Options = Options> = ExecaError<true, OptionsType>;
+export type ExecaError<OptionsType extends Options = Options> = ExecaCommonError & ExecaReturnValue<OptionsType>;
+export type ExecaSyncError<OptionsType extends SyncOptions = SyncOptions> = ExecaCommonError & ExecaSyncReturnValue<OptionsType>;
 
 export type KillOptions = {
 	/**
@@ -628,8 +636,8 @@ export type ExecaChildPromise<OptionsType extends Options = Options> = {
 	all?: Readable;
 
 	catch<ResultType = never>(
-		onRejected?: (reason: ExecaError<false, OptionsType>) => ResultType | PromiseLike<ResultType>
-	): Promise<ExecaReturnValue<false, OptionsType> | ResultType>;
+		onRejected?: (reason: ExecaError<OptionsType>) => ResultType | PromiseLike<ResultType>
+	): Promise<ExecaReturnValue<OptionsType> | ResultType>;
 
 	/**
 	Same as the original [`child_process#kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal), except if `signal` is `SIGTERM` (the default value) and the child process is not terminated after 5 seconds, force it by sending `SIGKILL`. Note that this graceful termination does not work on Windows, because Windows [doesn't support signals](https://nodejs.org/api/process.html#process_signal_events) (`SIGKILL` and `SIGTERM` has the same effect of force-killing the process immediately.) If you want to achieve graceful termination on Windows, you have to use other means, such as [`taskkill`](https://github.com/sindresorhus/taskkill).
@@ -673,7 +681,7 @@ export type ExecaChildPromise<OptionsType extends Options = Options> = {
 
 export type ExecaChildProcess<OptionsType extends Options = Options> = ChildProcess &
 ExecaChildPromise<OptionsType> &
-Promise<ExecaReturnValue<false, OptionsType>>;
+Promise<ExecaReturnValue<OptionsType>>;
 
 /**
 Executes a command using `file ...arguments`. `file` is a string or a file URL. `arguments` are an array of strings. Returns a `childProcess`.
@@ -786,12 +794,12 @@ setTimeout(() => {
 }, 1000);
 ```
 */
-export function execa<OptionsType extends Options<false> = {}>(
+export function execa<OptionsType extends Options = {}>(
 	file: string | URL,
 	arguments?: readonly string[],
 	options?: OptionsType,
 ): ExecaChildProcess<OptionsType>;
-export function execa<OptionsType extends Options<false> = {}>(
+export function execa<OptionsType extends Options = {}>(
 	file: string | URL,
 	options?: OptionsType,
 ): ExecaChildProcess<OptionsType>;
@@ -858,15 +866,15 @@ try {
 }
 ```
 */
-export function execaSync<OptionsType extends Options<true> = {}>(
+export function execaSync<OptionsType extends SyncOptions = {}>(
 	file: string | URL,
 	arguments?: readonly string[],
 	options?: OptionsType,
-): ExecaReturnValue<true, OptionsType>;
-export function execaSync<OptionsType extends Options<true> = {}>(
+): ExecaSyncReturnValue<OptionsType>;
+export function execaSync<OptionsType extends SyncOptions = {}>(
 	file: string | URL,
 	options?: OptionsType,
-): ExecaReturnValue<true, OptionsType>;
+): ExecaSyncReturnValue<OptionsType>;
 
 /**
 Executes a command. The `command` string includes both the `file` and its `arguments`. Returns a `childProcess`.
@@ -890,7 +898,7 @@ console.log(stdout);
 //=> 'unicorns'
 ```
 */
-export function execaCommand<OptionsType extends Options<false> = {}>(
+export function execaCommand<OptionsType extends Options = {}>(
 	command: string,
 	options?: OptionsType
 ): ExecaChildProcess<OptionsType>;
@@ -913,15 +921,15 @@ console.log(stdout);
 //=> 'unicorns'
 ```
 */
-export function execaCommandSync<OptionsType extends Options<true> = {}>(
+export function execaCommandSync<OptionsType extends SyncOptions = {}>(
 	command: string,
 	options?: OptionsType
-): ExecaReturnValue<true, OptionsType>;
+): ExecaSyncReturnValue<OptionsType>;
 
-type TemplateExpression = string | number | ExecaReturnValue
-| Array<string | number | ExecaReturnValue>;
+type TemplateExpression = string | number | ExecaCommonReturnValue
+| Array<string | number | ExecaCommonReturnValue>;
 
-type Execa$<OptionsType extends Options = {}> = {
+type Execa$<OptionsType extends CommonOptions = {}> = {
 	/**
 	Returns a new instance of `$` but with different default `options`. Consecutive calls are merged to previous ones.
 
@@ -945,13 +953,12 @@ type Execa$<OptionsType extends Options = {}> = {
 	//=> 'rainbows'
 	```
 	*/
-	<NewOptionsType extends Options = {}>
+	<NewOptionsType extends CommonOptions = {}>
 	(options: NewOptionsType):
 	Execa$<OptionsType & NewOptionsType>;
 
-	<NewOptionsType extends Options = {}>
 	(templates: TemplateStringsArray, ...expressions: TemplateExpression[]):
-	ExecaChildProcess<OptionsType & NewOptionsType>;
+	ExecaChildProcess<StricterOptions<OptionsType, Options>>;
 
 	/**
 	Same as $\`command\` but synchronous.
@@ -1003,7 +1010,7 @@ type Execa$<OptionsType extends Options = {}> = {
 	sync(
 		templates: TemplateStringsArray,
 		...expressions: TemplateExpression[]
-	): ExecaReturnValue<true, OptionsType>;
+	): ExecaSyncReturnValue<StricterOptions<OptionsType, SyncOptions>>;
 
 	/**
 	Same as $\`command\` but synchronous.
@@ -1055,7 +1062,7 @@ type Execa$<OptionsType extends Options = {}> = {
 	s(
 		templates: TemplateStringsArray,
 		...expressions: TemplateExpression[]
-	): ExecaReturnValue<true, OptionsType>;
+	): ExecaSyncReturnValue<StricterOptions<OptionsType, SyncOptions>>;
 };
 
 /**
@@ -1139,12 +1146,12 @@ import {execa} from 'execa';
 await execaNode('scriptPath', ['argument']);
 ```
 */
-export function execaNode<OptionsType extends NodeOptions<Options<false>> = {}>(
+export function execaNode<OptionsType extends NodeOptions = {}>(
 	scriptPath: string | URL,
 	arguments?: readonly string[],
 	options?: OptionsType
 ): ExecaChildProcess<OptionsType>;
-export function execaNode<OptionsType extends NodeOptions<Options<false>> = {}>(
+export function execaNode<OptionsType extends NodeOptions = {}>(
 	scriptPath: string | URL,
 	options?: OptionsType
 ): ExecaChildProcess<OptionsType>;
diff --git a/index.js b/index.js
index 1b934b6..d3dbb31 100644
--- a/index.js
+++ b/index.js
@@ -81,7 +81,7 @@ const handleArguments = (rawFile, rawArgs, rawOptions = {}) => {
 };
 
 const handleOutput = (options, value) => {
-	if (typeof value !== 'string' && !ArrayBuffer.isView(value)) {
+	if (value === undefined || value === null) {
 		return;
 	}
 
@@ -107,7 +107,6 @@ export function execa(rawFile, rawArgs, rawOptions) {
 		const errorPromise = Promise.reject(makeError({
 			error,
 			stdio: Array.from({length: stdioLength}),
-			all: options.all ? '' : undefined,
 			command,
 			escapedCommand,
 			options,
@@ -202,7 +201,7 @@ export function execaSync(rawFile, rawArgs, rawOptions) {
 
 	pipeOutputSync(stdioStreams, result);
 
-	const output = result.output || [undefined, undefined, undefined];
+	const output = result.output || Array.from({length: 3});
 	const stdio = output.map(stdioOutput => handleOutput(options, stdioOutput));
 
 	if (result.error || result.status !== 0 || result.signal !== null) {
diff --git a/lib/error.js b/lib/error.js
index d554bc3..e507e08 100644
--- a/lib/error.js
+++ b/lib/error.js
@@ -37,8 +37,6 @@ export const makeError = ({
 	isCanceled,
 	options: {timeout, cwd = process.cwd()},
 }) => {
-	stdio = stdio.map(stdioOutput => stdioOutput ?? '');
-
 	// `signal` and `exitCode` emitted on `spawned.on('exit')` event can be `null`.
 	// We normalize them to `undefined`
 	exitCode = exitCode === null ? undefined : exitCode;


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 70d5c22..bd1c576 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -240,7 +240,7 @@ try {
 	const ignoreFd3Result = await execa('unicorns', {stdio: ['pipe', 'pipe', 'pipe', 'ignore']});
 	expectType<undefined>(ignoreFd3Result.stdio[3]);
 } catch (error: unknown) {
-	const execaError = error as ExecaError<false>;
+	const execaError = error as ExecaError;
 
 	expectType<string>(execaError.message);
 	expectType<number | undefined>(execaError.exitCode);
@@ -256,75 +256,75 @@ try {
 
 	expectType<undefined>(execaError.stdio[0]);
 
-	const execaStringError = error as ExecaError<false, {}>;
+	const execaStringError = error as ExecaError<{}>;
 	expectType<string>(execaStringError.stdout);
 	expectType<string>(execaStringError.stdio[1]);
 	expectType<string>(execaStringError.stderr);
 	expectType<string>(execaStringError.stdio[2]);
 	expectType<string | undefined>(execaStringError.all);
 
-	const execaBufferError = error as ExecaError<false, {encoding: 'buffer'}>;
+	const execaBufferError = error as ExecaError<{encoding: 'buffer'}>;
 	expectType<Uint8Array>(execaBufferError.stdout);
 	expectType<Uint8Array>(execaBufferError.stdio[1]);
 	expectType<Uint8Array>(execaBufferError.stderr);
 	expectType<Uint8Array>(execaBufferError.stdio[2]);
 	expectType<Uint8Array | undefined>(execaBufferError.all);
 
-	const noBufferError = error as ExecaError<false, {buffer: false}>;
+	const noBufferError = error as ExecaError<{buffer: false}>;
 	expectType<undefined>(noBufferError.stdout);
 	expectType<undefined>(noBufferError.stdio[1]);
 	expectType<undefined>(noBufferError.stderr);
 	expectType<undefined>(noBufferError.stdio[2]);
 	expectType<undefined>(noBufferError.all);
 
-	const ignoreStdoutError = error as ExecaError<false, {stdout: 'ignore'}>;
+	const ignoreStdoutError = error as ExecaError<{stdout: 'ignore'}>;
 	expectType<undefined>(ignoreStdoutError.stdout);
 	expectType<undefined>(ignoreStdoutError.stdio[1]);
 	expectType<string>(ignoreStdoutError.stderr);
 	expectType<string>(ignoreStdoutError.stdio[2]);
 	expectType<string | undefined>(ignoreStdoutError.all);
 
-	const ignoreStderrError = error as ExecaError<false, {stderr: 'ignore'}>;
+	const ignoreStderrError = error as ExecaError<{stderr: 'ignore'}>;
 	expectType<string>(ignoreStderrError.stdout);
 	expectType<undefined>(ignoreStderrError.stderr);
 	expectType<string | undefined>(ignoreStderrError.all);
 
-	const inheritStdoutError = error as ExecaError<false, {stdout: 'inherit'}>;
+	const inheritStdoutError = error as ExecaError<{stdout: 'inherit'}>;
 	expectType<undefined>(inheritStdoutError.stdout);
 	expectType<string>(inheritStdoutError.stderr);
 	expectType<string | undefined>(inheritStdoutError.all);
 
-	const inheritStderrError = error as ExecaError<false, {stderr: 'inherit'}>;
+	const inheritStderrError = error as ExecaError<{stderr: 'inherit'}>;
 	expectType<string>(inheritStderrError.stdout);
 	expectType<undefined>(inheritStderrError.stderr);
 	expectType<string | undefined>(inheritStderrError.all);
 
-	const ipcStdoutError = error as ExecaError<false, {stdout: 'ipc'}>;
+	const ipcStdoutError = error as ExecaError<{stdout: 'ipc'}>;
 	expectType<undefined>(ipcStdoutError.stdout);
 	expectType<string>(ipcStdoutError.stderr);
 	expectType<string | undefined>(ipcStdoutError.all);
 
-	const ipcStderrError = error as ExecaError<false, {stderr: 'ipc'}>;
+	const ipcStderrError = error as ExecaError<{stderr: 'ipc'}>;
 	expectType<string>(ipcStderrError.stdout);
 	expectType<undefined>(ipcStderrError.stderr);
 	expectType<string | undefined>(ipcStderrError.all);
 
-	const numberStdoutError = error as ExecaError<false, {stdout: 1}>;
+	const numberStdoutError = error as ExecaError<{stdout: 1}>;
 	expectType<undefined>(numberStdoutError.stdout);
 	expectType<string>(numberStdoutError.stderr);
 	expectType<string | undefined>(numberStdoutError.all);
 
-	const numberStderrError = error as ExecaError<false, {stderr: 1}>;
+	const numberStderrError = error as ExecaError<{stderr: 1}>;
 	expectType<string>(numberStderrError.stdout);
 	expectType<undefined>(numberStderrError.stderr);
 	expectType<string | undefined>(numberStderrError.all);
 
-	const streamStdoutError = error as ExecaError<false, {stdout: typeof process.stdout}>;
+	const streamStdoutError = error as ExecaError<{stdout: typeof process.stdout}>;
 	expectType<undefined>(streamStdoutError.stdout);
 	expectType<string>(streamStdoutError.stderr);
 	expectType<string | undefined>(streamStdoutError.all);
 
-	const streamStderrError = error as ExecaError<false, {stderr: typeof process.stdout}>;
+	const streamStderrError = error as ExecaError<{stderr: typeof process.stdout}>;
 	expectType<string>(streamStderrError.stdout);
 	expectType<undefined>(streamStderrError.stderr);
 	expectType<string | undefined>(streamStderrError.all);
@@ -404,7 +404,7 @@ try {
 	expectType<undefined>(numberStderrResult.stderr);
 	expectError(numberStderrResult.all);
 } catch (error: unknown) {
-	const execaError = error as ExecaError<true>;
+	const execaError = error as ExecaSyncError;
 
 	expectType<ExecaSyncError>(execaError);
 	expectType<string>(execaError.message);
@@ -421,58 +421,58 @@ try {
 
 	expectType<undefined>(execaError.stdio[0]);
 
-	const execaStringError = error as ExecaError<true, {}>;
+	const execaStringError = error as ExecaSyncError<{}>;
 	expectType<string>(execaStringError.stdout);
 	expectType<string>(execaStringError.stdio[1]);
 	expectType<string>(execaStringError.stderr);
 	expectType<string>(execaStringError.stdio[2]);
 	expectError(execaStringError.all);
 
-	const execaBufferError = error as ExecaError<true, {encoding: 'buffer'}>;
+	const execaBufferError = error as ExecaSyncError<{encoding: 'buffer'}>;
 	expectType<Uint8Array>(execaBufferError.stdout);
 	expectType<Uint8Array>(execaBufferError.stdio[1]);
 	expectType<Uint8Array>(execaBufferError.stderr);
 	expectType<Uint8Array>(execaBufferError.stdio[2]);
 	expectError(execaBufferError.all);
 
-	const ignoreStdoutError = error as ExecaError<true, {stdout: 'ignore'}>;
+	const ignoreStdoutError = error as ExecaSyncError<{stdout: 'ignore'}>;
 	expectType<undefined>(ignoreStdoutError.stdout);
 	expectType<undefined>(ignoreStdoutError.stdio[1]);
 	expectType<string>(ignoreStdoutError.stderr);
 	expectType<string>(ignoreStdoutError.stdio[2]);
 	expectError(ignoreStdoutError.all);
 
-	const ignoreStderrError = error as ExecaError<true, {stderr: 'ignore'}>;
+	const ignoreStderrError = error as ExecaSyncError<{stderr: 'ignore'}>;
 	expectType<string>(ignoreStderrError.stdout);
 	expectType<undefined>(ignoreStderrError.stderr);
 	expectError(ignoreStderrError.all);
 
-	const inheritStdoutError = error as ExecaError<true, {stdout: 'inherit'}>;
+	const inheritStdoutError = error as ExecaSyncError<{stdout: 'inherit'}>;
 	expectType<undefined>(inheritStdoutError.stdout);
 	expectType<string>(inheritStdoutError.stderr);
 	expectError(inheritStdoutError.all);
 
-	const inheritStderrError = error as ExecaError<true, {stderr: 'inherit'}>;
+	const inheritStderrError = error as ExecaSyncError<{stderr: 'inherit'}>;
 	expectType<string>(inheritStderrError.stdout);
 	expectType<undefined>(inheritStderrError.stderr);
 	expectError(inheritStderrError.all);
 
-	const ipcStdoutError = error as ExecaError<true, {stdout: 'ipc'}>;
+	const ipcStdoutError = error as ExecaSyncError<{stdout: 'ipc'}>;
 	expectType<undefined>(ipcStdoutError.stdout);
 	expectType<string>(ipcStdoutError.stderr);
 	expectError(ipcStdoutError.all);
 
-	const ipcStderrError = error as ExecaError<true, {stderr: 'ipc'}>;
+	const ipcStderrError = error as ExecaSyncError<{stderr: 'ipc'}>;
 	expectType<string>(ipcStderrError.stdout);
 	expectType<undefined>(ipcStderrError.stderr);
 	expectError(ipcStderrError.all);
 
-	const numberStdoutError = error as ExecaError<true, {stdout: 1}>;
+	const numberStdoutError = error as ExecaSyncError<{stdout: 1}>;
 	expectType<undefined>(numberStdoutError.stdout);
 	expectType<string>(numberStdoutError.stderr);
 	expectError(numberStdoutError.all);
 
-	const numberStderrError = error as ExecaError<true, {stderr: 1}>;
+	const numberStderrError = error as ExecaSyncError<{stderr: 1}>;
 	expectType<string>(numberStderrError.stdout);
 	expectType<undefined>(numberStderrError.stderr);
 	expectError(numberStderrError.all);
@@ -919,28 +919,28 @@ execa('unicorns').kill('SIGKILL', {forceKillAfterTimeout: undefined});
 expectError(execa(['unicorns', 'arg']));
 expectType<ExecaChildProcess>(execa('unicorns'));
 expectType<ExecaChildProcess>(execa(fileUrl));
-expectType<ExecaReturnValue<false>>(await execa('unicorns'));
+expectType<ExecaReturnValue>(await execa('unicorns'));
 expectAssignable<{stdout: string}>(await execa('unicorns'));
 expectAssignable<{stdout: Uint8Array}>(await execa('unicorns', {encoding: 'buffer'}));
 expectAssignable<{stdout: string}>(await execa('unicorns', ['foo']));
 expectAssignable<{stdout: Uint8Array}>(await execa('unicorns', ['foo'], {encoding: 'buffer'}));
 
 expectError(execaSync(['unicorns', 'arg']));
-expectAssignable<ExecaReturnValue<true>>(execaSync('unicorns'));
-expectAssignable<ExecaReturnValue<true>>(execaSync(fileUrl));
+expectAssignable<ExecaSyncReturnValue>(execaSync('unicorns'));
+expectAssignable<ExecaSyncReturnValue>(execaSync(fileUrl));
 expectAssignable<{stdout: string}>(execaSync('unicorns'));
 expectAssignable<{stdout: Uint8Array}>(execaSync('unicorns', {encoding: 'buffer'}));
 expectAssignable<{stdout: string}>(execaSync('unicorns', ['foo']));
 expectAssignable<{stdout: Uint8Array}>(execaSync('unicorns', ['foo'], {encoding: 'buffer'}));
 
 expectType<ExecaChildProcess>(execaCommand('unicorns'));
-expectType<ExecaReturnValue<false>>(await execaCommand('unicorns'));
+expectType<ExecaReturnValue>(await execaCommand('unicorns'));
 expectAssignable<{stdout: string}>(await execaCommand('unicorns'));
 expectAssignable<{stdout: Uint8Array}>(await execaCommand('unicorns', {encoding: 'buffer'}));
 expectAssignable<{stdout: string}>(await execaCommand('unicorns foo'));
 expectAssignable<{stdout: Uint8Array}>(await execaCommand('unicorns foo', {encoding: 'buffer'}));
 
-expectAssignable<ExecaReturnValue<true>>(execaCommandSync('unicorns'));
+expectAssignable<ExecaSyncReturnValue>(execaCommandSync('unicorns'));
 expectAssignable<{stdout: string}>(execaCommandSync('unicorns'));
 expectAssignable<{stdout: Uint8Array}>(execaCommandSync('unicorns', {encoding: 'buffer'}));
 expectAssignable<{stdout: string}>(execaCommandSync('unicorns foo'));
@@ -948,8 +948,8 @@ expectAssignable<{stdout: Uint8Array}>(execaCommandSync('unicorns foo', {encodin
 
 expectError(execaNode(['unicorns', 'arg']));
 expectType<ExecaChildProcess>(execaNode('unicorns'));
-expectType<ExecaReturnValue<false>>(await execaNode('unicorns'));
-expectType<ExecaReturnValue<false>>(await execaNode(fileUrl));
+expectType<ExecaReturnValue>(await execaNode('unicorns'));
+expectType<ExecaReturnValue>(await execaNode(fileUrl));
 expectAssignable<{stdout: string}>(await execaNode('unicorns'));
 expectAssignable<{stdout: Uint8Array}>(await execaNode('unicorns', {encoding: 'buffer'}));
 expectAssignable<{stdout: string}>(await execaNode('unicorns', ['foo']));
@@ -963,10 +963,10 @@ expectAssignable<{stdout: Uint8Array}>(await execaNode('unicorns', {nodeOptions:
 expectAssignable<{stdout: string}>(await execaNode('unicorns', ['foo'], {nodeOptions: ['--async-stack-traces']}));
 expectAssignable<{stdout: Uint8Array}>(await execaNode('unicorns', ['foo'], {nodeOptions: ['--async-stack-traces'], encoding: 'buffer'}));
 
-expectType<ExecaChildProcess>($`unicorns`);
-expectType<ExecaReturnValue<false>>(await $`unicorns`);
-expectAssignable<ExecaReturnValue<true>>($.sync`unicorns`);
-expectAssignable<ExecaReturnValue<true>>($.s`unicorns`);
+expectAssignable<ExecaChildProcess>($`unicorns`);
+expectAssignable<ExecaReturnValue>(await $`unicorns`);
+expectAssignable<ExecaSyncReturnValue>($.sync`unicorns`);
+expectAssignable<ExecaSyncReturnValue>($.s`unicorns`);
 
 expectAssignable<ExecaChildProcess>($({})`unicorns`);
 expectAssignable<{stdout: string}>(await $({})`unicorns`);
@@ -1000,21 +1000,21 @@ expectAssignable<ExecaChildProcess>($({})({encoding: 'buffer'})`unicorns foo`);
 expectAssignable<{stdout: Uint8Array}>(await $({})({encoding: 'buffer'})`unicorns foo`);
 expectAssignable<{stdout: Uint8Array}>($({})({encoding: 'buffer'}).sync`unicorns foo`);
 
-expectAssignable<ExecaReturnValue<false>>(await $`unicorns ${'foo'}`);
-expectAssignable<ExecaReturnValue<true>>($.sync`unicorns ${'foo'}`);
-expectAssignable<ExecaReturnValue<false>>(await $`unicorns ${1}`);
-expectAssignable<ExecaReturnValue<true>>($.sync`unicorns ${1}`);
-expectAssignable<ExecaReturnValue<false>>(await $`unicorns ${['foo', 'bar']}`);
-expectAssignable<ExecaReturnValue<true>>($.sync`unicorns ${['foo', 'bar']}`);
-expectAssignable<ExecaReturnValue<false>>(await $`unicorns ${[1, 2]}`);
-expectAssignable<ExecaReturnValue<true>>($.sync`unicorns ${[1, 2]}`);
-expectAssignable<ExecaReturnValue<false>>(await $`unicorns ${await $`echo foo`}`);
-expectError<ExecaReturnValue<false>>(await $`unicorns ${$`echo foo`}`);
-expectAssignable<ExecaReturnValue<true>>($.sync`unicorns ${$.sync`echo foo`}`);
-expectAssignable<ExecaReturnValue<false>>(await $`unicorns ${[await $`echo foo`, 'bar']}`);
-expectError<ExecaReturnValue<false>>(await $`unicorns ${[$`echo foo`, 'bar']}`);
-expectAssignable<ExecaReturnValue<true>>($.sync`unicorns ${[$.sync`echo foo`, 'bar']}`);
-expectAssignable<ExecaReturnValue<false>>(await $`unicorns ${true.toString()}`);
-expectAssignable<ExecaReturnValue<true>>($.sync`unicorns ${false.toString()}`);
-expectError<ExecaReturnValue<false>>(await $`unicorns ${true}`);
-expectError<ExecaReturnValue<true>>($.sync`unicorns ${false}`);
+expectAssignable<ExecaReturnValue>(await $`unicorns ${'foo'}`);
+expectAssignable<ExecaSyncReturnValue>($.sync`unicorns ${'foo'}`);
+expectAssignable<ExecaReturnValue>(await $`unicorns ${1}`);
+expectAssignable<ExecaSyncReturnValue>($.sync`unicorns ${1}`);
+expectAssignable<ExecaReturnValue>(await $`unicorns ${['foo', 'bar']}`);
+expectAssignable<ExecaSyncReturnValue>($.sync`unicorns ${['foo', 'bar']}`);
+expectAssignable<ExecaReturnValue>(await $`unicorns ${[1, 2]}`);
+expectAssignable<ExecaSyncReturnValue>($.sync`unicorns ${[1, 2]}`);
+expectAssignable<ExecaReturnValue>(await $`unicorns ${await $`echo foo`}`);
+expectError<ExecaReturnValue>(await $`unicorns ${$`echo foo`}`);
+expectAssignable<ExecaSyncReturnValue>($.sync`unicorns ${$.sync`echo foo`}`);
+expectAssignable<ExecaReturnValue>(await $`unicorns ${[await $`echo foo`, 'bar']}`);
+expectError<ExecaReturnValue>(await $`unicorns ${[$`echo foo`, 'bar']}`);
+expectAssignable<ExecaSyncReturnValue>($.sync`unicorns ${[$.sync`echo foo`, 'bar']}`);
+expectAssignable<ExecaReturnValue>(await $`unicorns ${true.toString()}`);
+expectAssignable<ExecaSyncReturnValue>($.sync`unicorns ${false.toString()}`);
+expectError<ExecaReturnValue>(await $`unicorns ${true}`);
+expectError<ExecaSyncReturnValue>($.sync`unicorns ${false}`);
diff --git a/test/error.js b/test/error.js
index da8c4d3..40f5b01 100644
--- a/test/error.js
+++ b/test/error.js
@@ -10,65 +10,72 @@ setFixtureDir();
 
 const TIMEOUT_REGEXP = /timed out after/;
 
-test('empty error.stdout/stderr/stdio', async t => {
-	const {stdout, stderr, stdio} = await t.throwsAsync(execa('fail.js'));
+const testEmptyErrorStdio = async (t, execaMethod) => {
+	const {failed, stdout, stderr, stdio} = await execaMethod('fail.js', {reject: false});
+	t.true(failed);
 	t.is(stdout, '');
 	t.is(stderr, '');
-	t.deepEqual(stdio, ['', '', '']);
-});
+	t.deepEqual(stdio, [undefined, '', '']);
+};
 
-test('empty error.all', async t => {
-	const {all} = await t.throwsAsync(execa('fail.js', {all: true}));
-	t.is(all, '');
-});
+test('empty error.stdout/stderr/stdio', testEmptyErrorStdio, execa);
+test('empty error.stdout/stderr/stdio - sync', testEmptyErrorStdio, execaSync);
 
-test('undefined error.all', async t => {
-	const {all} = await t.throwsAsync(execa('fail.js'));
-	t.is(all, undefined);
-});
+const testUndefinedErrorStdio = async (t, execaMethod) => {
+	const {stdout, stderr, stdio} = await execaMethod('empty.js', {stdio: 'ignore'});
+	t.is(stdout, undefined);
+	t.is(stderr, undefined);
+	t.deepEqual(stdio, [undefined, undefined, undefined]);
+};
+
+test('undefined error.stdout/stderr/stdio', testUndefinedErrorStdio, execa);
+test('undefined error.stdout/stderr/stdio - sync', testUndefinedErrorStdio, execaSync);
+
+const testEmptyAll = async (t, options, expectedValue) => {
+	const {all} = await t.throwsAsync(execa('fail.js', options));
+	t.is(all, expectedValue);
+};
+
+test('empty error.all', testEmptyAll, {all: true}, '');
+test('undefined error.all', testEmptyAll, {}, undefined);
+test('ignored error.all', testEmptyAll, {all: true, stdio: 'ignore'}, undefined);
 
 test('empty error.stdio[0] even with input', async t => {
 	const {stdio} = await t.throwsAsync(execa('fail.js', {input: 'test'}));
-	t.is(stdio[0], '');
+	t.is(stdio[0], undefined);
 });
 
-const WRONG_COMMAND = isWindows
-	? '\'wrong\' is not recognized as an internal or external command,\r\noperable program or batch file.'
-	: '';
+// `error.code` is OS-specific here
+const SPAWN_ERROR_CODES = new Set(['EINVAL', 'ENOTSUP', 'EPERM']);
 
-test('stdout/stderr/all/stdio on process errors', async t => {
-	const {stdout, stderr, all, stdio} = await t.throwsAsync(execa('wrong command', {all: true}));
-	t.is(stdout, '');
-	t.is(stderr, WRONG_COMMAND);
-	t.is(all, WRONG_COMMAND);
-	t.deepEqual(stdio, ['', '', WRONG_COMMAND]);
+test('stdout/stderr/stdio on process spawning errors', async t => {
+	const {code, stdout, stderr, stdio} = await t.throwsAsync(execa('noop.js', {uid: -1}));
+	t.true(SPAWN_ERROR_CODES.has(code));
+	t.is(stdout, undefined);
+	t.is(stderr, undefined);
+	t.deepEqual(stdio, [undefined, undefined, undefined]);
 });
 
-test('stdout/stderr/all/stdio on process errors, in sync mode', t => {
-	const {stdout, stderr, all, stdio} = t.throws(() => {
-		execaSync('wrong command');
+test('stdout/stderr/all/stdio on process spawning errors - sync', t => {
+	const {code, stdout, stderr, stdio} = t.throws(() => {
+		execaSync('noop.js', {uid: -1});
 	});
-	t.is(stdout, '');
-	t.is(stderr, WRONG_COMMAND);
-	t.is(all, undefined);
-	t.deepEqual(stdio, ['', '', WRONG_COMMAND]);
+	t.true(SPAWN_ERROR_CODES.has(code));
+	t.is(stdout, undefined);
+	t.is(stderr, undefined);
+	t.deepEqual(stdio, [undefined, undefined, undefined]);
 });
 
-test('error.stdout/stderr/stdio is defined', async t => {
-	const {stdout, stderr, stdio} = await t.throwsAsync(execa('echo-fail.js', fullStdio));
+const testErrorOutput = async (t, execaMethod) => {
+	const {failed, stdout, stderr, stdio} = await execaMethod('echo-fail.js', {...fullStdio, reject: false});
+	t.true(failed);
 	t.is(stdout, 'stdout');
 	t.is(stderr, 'stderr');
-	t.deepEqual(stdio, ['', 'stdout', 'stderr', 'fd3']);
-});
+	t.deepEqual(stdio, [undefined, 'stdout', 'stderr', 'fd3']);
+};
 
-test('error.stdout/stderr/stdio is defined, in sync mode', t => {
-	const {stdout, stderr, stdio} = t.throws(() => {
-		execaSync('echo-fail.js', fullStdio);
-	});
-	t.is(stdout, 'stdout');
-	t.is(stderr, 'stderr');
-	t.deepEqual(stdio, ['', 'stdout', 'stderr', 'fd3']);
-});
+test('error.stdout/stderr/stdio is defined', testErrorOutput, execa);
+test('error.stdout/stderr/stdio is defined - sync', testErrorOutput, execaSync);
 
 test('exitCode is 0 on success', async t => {
 	const {exitCode} = await execa('noop.js', ['foo']);
@@ -76,7 +83,10 @@ test('exitCode is 0 on success', async t => {
 });
 
 const testExitCode = async (t, number) => {
-	const {exitCode} = await t.throwsAsync(execa('exit.js', [`${number}`]), {message: new RegExp(`failed with exit code ${number}`)});
+	const {exitCode} = await t.throwsAsync(
+		execa('exit.js', [`${number}`]),
+		{message: new RegExp(`failed with exit code ${number}`)},
+	);
 	t.is(exitCode, number);
 };