Skip to content

Commit 1fd9db4

Browse files
ehmickysindresorhus
authored andcommitted
Improve returned error.message (#180)
1 parent eac23b0 commit 1fd9db4

File tree

2 files changed

+75
-39
lines changed

2 files changed

+75
-39
lines changed

index.js

+48-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22
const path = require('path');
3+
const os = require('os');
34
const childProcess = require('child_process');
45
const crossSpawn = require('cross-spawn');
56
const stripFinalNewline = require('strip-final-newline');
@@ -120,43 +121,67 @@ function getStream(process, stream, {encoding, buffer, maxBuffer}) {
120121
}
121122

122123
function makeError(result, options) {
123-
const {stdout, stderr} = result;
124-
124+
const {stdout, stderr, code, signal} = result;
125125
let {error} = result;
126-
const {code, signal} = result;
127-
128-
const {parsed, joinedCommand} = options;
129-
const timedOut = options.timedOut || false;
126+
const {joinedCommand, timedOut, parsed: {options: {timeout}}} = options;
130127

131-
if (!error) {
132-
let output = '';
133-
134-
if (Array.isArray(parsed.options.stdio)) {
135-
if (parsed.options.stdio[2] !== 'inherit') {
136-
output += output.length > 0 ? stderr : `\n${stderr}`;
137-
}
138-
139-
if (parsed.options.stdio[1] !== 'inherit') {
140-
output += `\n${stdout}`;
141-
}
142-
} else if (parsed.options.stdio !== 'inherit') {
143-
output = `\n${stderr}${stdout}`;
144-
}
128+
const [codeString, codeNumber] = getCode(result, code);
145129

146-
error = new Error(`Command failed: ${joinedCommand}${output}`);
147-
error.code = code < 0 ? errname(code) : code;
130+
if (!(error instanceof Error)) {
131+
const message = [joinedCommand, stderr, stdout].filter(Boolean).join('\n');
132+
error = new Error(message);
148133
}
149134

135+
const prefix = getErrorPrefix({timedOut, timeout, signal, codeString, codeNumber});
136+
error.message = `Command ${prefix}: ${error.message}`;
137+
138+
error.code = codeNumber || codeString;
150139
error.stdout = stdout;
151140
error.stderr = stderr;
152141
error.failed = true;
153142
error.signal = signal || null;
154143
error.cmd = joinedCommand;
155-
error.timedOut = timedOut;
144+
error.timedOut = Boolean(timedOut);
156145

157146
return error;
158147
}
159148

149+
function getCode({error = {}}, code) {
150+
if (error.code) {
151+
return [error.code, os.constants.errno[error.code]];
152+
}
153+
154+
if (Number.isInteger(code)) {
155+
return [errname(-Math.abs(code)), Math.abs(code)];
156+
}
157+
158+
return [];
159+
}
160+
161+
function getErrorPrefix({timedOut, timeout, signal, codeString, codeNumber}) {
162+
if (timedOut) {
163+
return `timed out after ${timeout} milliseconds`;
164+
}
165+
166+
if (signal) {
167+
return `was killed with ${signal}`;
168+
}
169+
170+
if (codeString !== undefined && codeNumber !== undefined) {
171+
return `failed with exit code ${codeNumber} (${codeString})`;
172+
}
173+
174+
if (codeString !== undefined) {
175+
return `failed with exit code ${codeString}`;
176+
}
177+
178+
if (codeNumber !== undefined) {
179+
return `failed with exit code ${codeNumber}`;
180+
}
181+
182+
return 'failed';
183+
}
184+
160185
function joinCommand(command, args) {
161186
let joinedCommand = command;
162187

test.js

+27-16
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ process.env.FOO = 'foo';
1414

1515
const NO_NEWLINES_REGEXP = /^[^\n]*$/;
1616
const STDERR_STDOUT_REGEXP = /stderr[^]*stdout/;
17+
const TIMEOUT_REGEXP = /timed out after/;
18+
19+
function getExitRegExp(exitMessage) {
20+
return new RegExp(`failed with exit code ${exitMessage}`);
21+
}
1722

1823
test('execa()', async t => {
1924
const {stdout} = await m('noop', ['foo']);
@@ -45,7 +50,7 @@ test('execa.stderr()', async t => {
4550
});
4651

4752
test('stdout/stderr available on errors', async t => {
48-
const err = await t.throwsAsync(m('exit', ['2']));
53+
const err = await t.throwsAsync(m('exit', ['2']), {message: getExitRegExp('2')});
4954
t.is(typeof err.stdout, 'string');
5055
t.is(typeof err.stderr, 'string');
5156
});
@@ -99,7 +104,7 @@ test('execa.sync()', t => {
99104
});
100105

101106
test('execa.sync() throws error if written to stderr', t => {
102-
t.throws(() => m.sync('foo'), process.platform === 'win32' ? /'foo' is not recognized as an internal or external command/ : 'spawnSync foo ENOENT');
107+
t.throws(() => m.sync('foo'), process.platform === 'win32' ? /'foo' is not recognized as an internal or external command/ : /spawnSync foo ENOENT/);
103108
});
104109

105110
test('execa.sync() includes stdout and stderr in errors for improved debugging', t => {
@@ -256,9 +261,13 @@ test('skip throwing when using reject option', async t => {
256261
t.is(typeof error.stderr, 'string');
257262
});
258263

264+
test('allow unknown exit code', async t => {
265+
await t.throwsAsync(m('exit', ['255']), {message: /exit code 255 \(Unknown system error -255\)/});
266+
});
267+
259268
test('execa() returns code and failed properties', async t => {
260269
const {code, failed} = await m('noop', ['foo']);
261-
const error = await t.throwsAsync(m('exit', ['2']), {code: 2});
270+
const error = await t.throwsAsync(m('exit', ['2']), {code: 2, message: getExitRegExp('2')});
262271
t.is(code, 0);
263272
t.false(failed);
264273
t.true(error.failed);
@@ -284,7 +293,7 @@ test('error.killed is true if process was killed directly', async t => {
284293
cp.kill();
285294
}, 100);
286295

287-
const error = await t.throwsAsync(cp);
296+
const error = await t.throwsAsync(cp, {message: /was killed with SIGTERM/});
288297
t.true(error.killed);
289298
});
290299

@@ -296,7 +305,9 @@ test('error.killed is false if process was killed indirectly', async t => {
296305
process.kill(cp.pid, 'SIGINT');
297306
}, 100);
298307

299-
const error = await t.throwsAsync(cp);
308+
// `process.kill()` is emulated by Node.js on Windows
309+
const message = process.platform === 'win32' ? /failed with exit code 1/ : /was killed with SIGINT/;
310+
const error = await t.throwsAsync(cp, {message});
300311
t.false(error.killed);
301312
});
302313

@@ -322,7 +333,7 @@ if (process.platform !== 'win32') {
322333
process.kill(cp.pid, 'SIGINT');
323334
}, 100);
324335

325-
const error = await t.throwsAsync(cp);
336+
const error = await t.throwsAsync(cp, {message: /was killed with SIGINT/});
326337
t.is(error.signal, 'SIGINT');
327338
});
328339

@@ -333,12 +344,12 @@ if (process.platform !== 'win32') {
333344
process.kill(cp.pid, 'SIGTERM');
334345
}, 100);
335346

336-
const error = await t.throwsAsync(cp);
347+
const error = await t.throwsAsync(cp, {message: /was killed with SIGTERM/});
337348
t.is(error.signal, 'SIGTERM');
338349
});
339350

340351
test('custom error.signal', async t => {
341-
const error = await t.throwsAsync(m('delay', ['3000', '0'], {killSignal: 'SIGHUP', timeout: 1500}));
352+
const error = await t.throwsAsync(m('delay', ['3000', '0'], {killSignal: 'SIGHUP', timeout: 1500, message: TIMEOUT_REGEXP}));
342353
t.is(error.signal, 'SIGHUP');
343354
});
344355
}
@@ -348,27 +359,27 @@ test('result.signal is null for successful execution', async t => {
348359
});
349360

350361
test('result.signal is null if process failed, but was not killed', async t => {
351-
const error = await t.throwsAsync(m('exit', [2]));
362+
const error = await t.throwsAsync(m('exit', [2]), {message: getExitRegExp('2')});
352363
t.is(error.signal, null);
353364
});
354365

355366
async function code(t, num) {
356-
await t.throwsAsync(m('exit', [`${num}`]), {code: num});
367+
await t.throwsAsync(m('exit', [`${num}`]), {code: num, message: getExitRegExp(num)});
357368
}
358369

359370
test('error.code is 2', code, 2);
360371
test('error.code is 3', code, 3);
361372
test('error.code is 4', code, 4);
362373

363374
test('timeout will kill the process early', async t => {
364-
const error = await t.throwsAsync(m('delay', ['60000', '0'], {timeout: 1500}));
375+
const error = await t.throwsAsync(m('delay', ['60000', '0'], {timeout: 1500, message: TIMEOUT_REGEXP}));
365376

366377
t.true(error.timedOut);
367378
t.not(error.code, 22);
368379
});
369380

370381
test('timeout will not kill the process early', async t => {
371-
const error = await t.throwsAsync(m('delay', ['3000', '22'], {timeout: 30000}), {code: 22});
382+
const error = await t.throwsAsync(m('delay', ['3000', '22'], {timeout: 30000}), {code: 22, message: getExitRegExp('22')});
372383
t.false(error.timedOut);
373384
});
374385

@@ -378,7 +389,7 @@ test('timedOut will be false if no timeout was set and zero exit code', async t
378389
});
379390

380391
test('timedOut will be false if no timeout was set and non-zero exit code', async t => {
381-
const error = await t.throwsAsync(m('delay', ['1000', '3']));
392+
const error = await t.throwsAsync(m('delay', ['1000', '3']), {message: getExitRegExp('3')});
382393
t.false(error.timedOut);
383394
});
384395

@@ -388,8 +399,8 @@ async function errorMessage(t, expected, ...args) {
388399

389400
errorMessage.title = (message, expected) => `error.message matches: ${expected}`;
390401

391-
test(errorMessage, /Command failed: exit 2 foo bar/, 2, 'foo', 'bar');
392-
test(errorMessage, /Command failed: exit 3 baz quz/, 3, 'baz', 'quz');
402+
test(errorMessage, /Command failed with exit code 2.*: exit 2 foo bar/, 2, 'foo', 'bar');
403+
test(errorMessage, /Command failed with exit code 3.*: exit 3 baz quz/, 3, 'baz', 'quz');
393404

394405
async function cmd(t, expected, ...args) {
395406
const error = await t.throwsAsync(m('fail', args));
@@ -453,7 +464,7 @@ if (process.platform !== 'win32') {
453464
await m(`fast-exit-${process.platform}`, [], {input: 'data'});
454465
t.pass();
455466
} catch (error) {
456-
t.is(error.code, 'EPIPE');
467+
t.is(error.code, 32);
457468
}
458469
});
459470
}

0 commit comments

Comments
 (0)