Skip to content

Commit f11e42b

Browse files
authored
feat: add throwOnError to throw errors on non-zero exits (#34)
Introduces a new `throwOnError` option which will cause tinyexec to throw any time a non-zero exit code is encountered. If the exit code is `null`, we will not throw since it means something went very wrong anyway (the process is still running and shouldn't be, since we saw the `close` event by then). If the exit code is greater than `0`, we will throw a `NonZeroExitError` which has an `exitCode` property. For example: ```ts try { await x('foo', [], {throwOnError: true}); } catch (err) { if (err instanceof NonZeroExitCode) { err.exitCode; // the exit code err.result; // the tinyexec process err.result.killed; // getters on tinyexec process } } ```
1 parent 64154fe commit f11e42b

File tree

4 files changed

+70
-2
lines changed

4 files changed

+70
-2
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ The options object can have the following properties:
5353
- `persist` - if `true`, the process will continue after the host exits
5454
- `stdin` - another `Result` can be used as the input to this process
5555
- `nodeOptions` - any valid options to node's underlying `spawn` function
56+
- `throwOnError` - if true, non-zero exit codes will throw an error
5657

5758
### Piping to another process
5859

src/main.ts

+20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {computeEnv} from './env.js';
66
import {combineStreams} from './stream.js';
77
import readline from 'node:readline';
88
import {_parse} from 'cross-spawn';
9+
import {NonZeroExitError} from './non-zero-exit-error.js';
10+
11+
export {NonZeroExitError};
912

1013
export interface Output {
1114
stderr: string;
@@ -40,6 +43,7 @@ export interface Options {
4043
timeout: number;
4144
persist: boolean;
4245
stdin: ExecProcess;
46+
throwOnError: boolean;
4347
}
4448

4549
export interface TinyExec {
@@ -188,6 +192,14 @@ export class ExecProcess implements Result {
188192
if (this._thrownError) {
189193
throw this._thrownError;
190194
}
195+
196+
if (
197+
this._options?.throwOnError &&
198+
this.exitCode !== 0 &&
199+
this.exitCode !== undefined
200+
) {
201+
throw new NonZeroExitError(this);
202+
}
191203
}
192204

193205
protected async _waitForOutput(): Promise<Output> {
@@ -229,6 +241,14 @@ export class ExecProcess implements Result {
229241
stdout
230242
};
231243

244+
if (
245+
this._options.throwOnError &&
246+
this.exitCode !== 0 &&
247+
this.exitCode !== undefined
248+
) {
249+
throw new NonZeroExitError(this, result);
250+
}
251+
232252
return result;
233253
}
234254

src/non-zero-exit-error.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type {Result, Output} from './main.js';
2+
3+
export class NonZeroExitError extends Error {
4+
public readonly result: Result;
5+
public readonly output?: Output;
6+
7+
public get exitCode(): number | undefined {
8+
if (this.result.exitCode !== null) {
9+
return this.result.exitCode;
10+
}
11+
return undefined;
12+
}
13+
14+
public constructor(result: Result, output?: Output) {
15+
super(`Process exited with non-zero status (${result.exitCode})`);
16+
17+
this.result = result;
18+
this.output = output;
19+
}
20+
}

src/test/main_test.ts

+29-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {x} from '../main.js';
1+
import {x, NonZeroExitError} from '../main.js';
22
import * as assert from 'node:assert/strict';
33
import {test} from 'node:test';
44
import os from 'node:os';
@@ -19,6 +19,14 @@ test('exec', async (t) => {
1919
assert.equal(proc.exitCode, 0);
2020
});
2121

22+
await t.test('non-zero exitCode throws when throwOnError=true', async () => {
23+
const proc = x('node', ['-e', 'process.exit(1);'], {throwOnError: true});
24+
await assert.rejects(async () => {
25+
await proc;
26+
}, NonZeroExitError);
27+
assert.equal(proc.exitCode, 1);
28+
});
29+
2230
await t.test('async iterator gets correct output', async () => {
2331
const proc = x('node', ['-e', "console.log('foo'); console.log('bar');"]);
2432
const lines = [];
@@ -64,6 +72,25 @@ if (isWindows) {
6472
assert.equal(result.stdout, '');
6573
});
6674

75+
await t.test('throws spawn errors when throwOnError=true', async () => {
76+
const proc = x('definitelyNonExistent', [], {throwOnError: true});
77+
await assert.rejects(
78+
async () => {
79+
await proc;
80+
},
81+
(err) => {
82+
assert.ok(err instanceof NonZeroExitError);
83+
assert.equal(
84+
err.output?.stderr,
85+
"'definitelyNonExistent' is not recognized as an internal" +
86+
' or external command,\r\noperable program or batch file.\r\n'
87+
);
88+
assert.equal(err.output?.stdout, '');
89+
return true;
90+
}
91+
);
92+
});
93+
6794
await t.test('kill terminates the process', async () => {
6895
// Somewhat filthy way of waiting for 2 seconds across cmd/ps
6996
const proc = x('ping', ['127.0.0.1', '-n', '2']);
@@ -100,7 +127,7 @@ if (isWindows) {
100127

101128
await t.test('async iterator receives errors as lines', async () => {
102129
const proc = x('nonexistentforsure');
103-
const lines = [];
130+
const lines: string[] = [];
104131
for await (const line of proc) {
105132
lines.push(line);
106133
}

0 commit comments

Comments
 (0)