Repository: sindresorhus/execa
Pull request #229 (2019-05-10 12:00:36)

Make `error.killed` and `error.isCanceled` always boolean (not `undefined`)
The `killed` and `isCanceled` properties of `error` should always be `true` or `false`, not `undefined`. This makes the interface simpler and more predictable.

This PR solves the use cases where it could have been `undefined`, e.g. when using `execa.sync()`.

It also adds related tests.

sindresorhus: Can you fix the merge conflict?
ehmicky: Fixed.

Patch:
diff --git a/index.js b/index.js
index dc0f983..ca2148a 100644
--- a/index.js
+++ b/index.js
@@ -196,7 +196,7 @@ function makeError(result, options) {
 	// it to `undefined`
 	error.signal = signal || undefined;
 	error.command = joinedCommand;
-	error.timedOut = Boolean(timedOut);
+	error.timedOut = timedOut;
 	error.isCanceled = isCanceled;
 
 	if ('all' in result) {
@@ -449,7 +449,9 @@ module.exports.sync = (command, args, options) => {
 	if (result.error || result.status !== 0 || result.signal !== null) {
 		const error = makeError(result, {
 			joinedCommand,
-			parsed
+			parsed,
+			timedOut: false,
+			isCanceled: false
 		});
 
 		if (!parsed.options.reject) {
@@ -466,7 +468,9 @@ module.exports.sync = (command, args, options) => {
 		exitCode: 0,
 		exitCodeName: 'SUCCESS',
 		failed: false,
+		killed: false,
 		command: joinedCommand,
-		timedOut: false
+		timedOut: false,
+		isCanceled: false
 	};
 };


Test patch:
diff --git a/test.js b/test.js
index 5d7a4b4..ee83fae 100644
--- a/test.js
+++ b/test.js
@@ -305,6 +305,16 @@ test('error.killed is false if process was killed indirectly', async t => {
 	t.false(error.killed);
 });
 
+test('result.killed is false if not killed', async t => {
+	const result = await execa('noop');
+	t.false(result.killed);
+});
+
+test('result.killed is false if not killed, in sync mode', t => {
+	const result = execa.sync('noop');
+	t.false(result.killed);
+});
+
 if (process.platform === 'darwin') {
 	test.cb('sanity check: child_process.exec also has killed.false if killed indirectly', t => {
 		const cp = childProcess.exec('forever', error => {
@@ -375,6 +385,11 @@ test('timedOut is false if no timeout was set', async t => {
 	t.false(result.timedOut);
 });
 
+test('timedOut will be false if no timeout was set and zero exit code in sync mode', t => {
+	const result = execa.sync('noop');
+	t.false(result.timedOut);
+});
+
 async function errorMessage(t, expected, ...args) {
 	await t.throwsAsync(execa('exit', args), {message: expected});
 }
@@ -578,11 +593,26 @@ test('cancel method kills the subprocess', t => {
 	t.true(subprocess.killed);
 });
 
-test('result.isCanceled is false when spawned.cancel isn\'t called', async t => {
+test('result.isCanceled is false when spawned.cancel() isn\'t called (success)', async t => {
 	const result = await execa('noop');
 	t.false(result.isCanceled);
 });
 
+test('result.isCanceled is false when spawned.cancel() isn\'t called (failure)', async t => {
+	const error = await t.throwsAsync(execa('fail'));
+	t.false(error.isCanceled);
+});
+
+test('result.isCanceled is false when spawned.cancel() isn\'t called in sync mode (success)', t => {
+	const result = execa.sync('noop');
+	t.false(result.isCanceled);
+});
+
+test('result.isCanceled is false when spawned.cancel() isn\'t called in sync mode (failure)', t => {
+	const error = t.throws(() => execa.sync('fail'));
+	t.false(error.isCanceled);
+});
+
 test('calling cancel method throws an error with message "Command was canceled"', async t => {
 	const subprocess = execa('noop');
 	subprocess.cancel();