Skip to content

Commit

Permalink
feat: add throwOnError to throw errors on non-zero exits (#34)
Browse files Browse the repository at this point in the history
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
  }
}
```
  • Loading branch information
43081j authored Aug 25, 2024
1 parent 64154fe commit f11e42b
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ The options object can have the following properties:
- `persist` - if `true`, the process will continue after the host exits
- `stdin` - another `Result` can be used as the input to this process
- `nodeOptions` - any valid options to node's underlying `spawn` function
- `throwOnError` - if true, non-zero exit codes will throw an error

### Piping to another process

Expand Down
20 changes: 20 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {computeEnv} from './env.js';
import {combineStreams} from './stream.js';
import readline from 'node:readline';
import {_parse} from 'cross-spawn';
import {NonZeroExitError} from './non-zero-exit-error.js';

export {NonZeroExitError};

export interface Output {
stderr: string;
Expand Down Expand Up @@ -40,6 +43,7 @@ export interface Options {
timeout: number;
persist: boolean;
stdin: ExecProcess;
throwOnError: boolean;
}

export interface TinyExec {
Expand Down Expand Up @@ -188,6 +192,14 @@ export class ExecProcess implements Result {
if (this._thrownError) {
throw this._thrownError;
}

if (
this._options?.throwOnError &&
this.exitCode !== 0 &&
this.exitCode !== undefined
) {
throw new NonZeroExitError(this);
}
}

protected async _waitForOutput(): Promise<Output> {
Expand Down Expand Up @@ -229,6 +241,14 @@ export class ExecProcess implements Result {
stdout
};

if (
this._options.throwOnError &&
this.exitCode !== 0 &&
this.exitCode !== undefined
) {
throw new NonZeroExitError(this, result);
}

return result;
}

Expand Down
20 changes: 20 additions & 0 deletions src/non-zero-exit-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type {Result, Output} from './main.js';

export class NonZeroExitError extends Error {
public readonly result: Result;
public readonly output?: Output;

public get exitCode(): number | undefined {
if (this.result.exitCode !== null) {
return this.result.exitCode;
}
return undefined;
}

public constructor(result: Result, output?: Output) {
super(`Process exited with non-zero status (${result.exitCode})`);

this.result = result;
this.output = output;
}
}
31 changes: 29 additions & 2 deletions src/test/main_test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {x} from '../main.js';
import {x, NonZeroExitError} from '../main.js';
import * as assert from 'node:assert/strict';
import {test} from 'node:test';
import os from 'node:os';
Expand All @@ -19,6 +19,14 @@ test('exec', async (t) => {
assert.equal(proc.exitCode, 0);
});

await t.test('non-zero exitCode throws when throwOnError=true', async () => {
const proc = x('node', ['-e', 'process.exit(1);'], {throwOnError: true});
await assert.rejects(async () => {
await proc;
}, NonZeroExitError);
assert.equal(proc.exitCode, 1);
});

await t.test('async iterator gets correct output', async () => {
const proc = x('node', ['-e', "console.log('foo'); console.log('bar');"]);
const lines = [];
Expand Down Expand Up @@ -64,6 +72,25 @@ if (isWindows) {
assert.equal(result.stdout, '');
});

await t.test('throws spawn errors when throwOnError=true', async () => {
const proc = x('definitelyNonExistent', [], {throwOnError: true});
await assert.rejects(
async () => {
await proc;
},
(err) => {
assert.ok(err instanceof NonZeroExitError);
assert.equal(
err.output?.stderr,
"'definitelyNonExistent' is not recognized as an internal" +
' or external command,\r\noperable program or batch file.\r\n'
);
assert.equal(err.output?.stdout, '');
return true;
}
);
});

await t.test('kill terminates the process', async () => {
// Somewhat filthy way of waiting for 2 seconds across cmd/ps
const proc = x('ping', ['127.0.0.1', '-n', '2']);
Expand Down Expand Up @@ -100,7 +127,7 @@ if (isWindows) {

await t.test('async iterator receives errors as lines', async () => {
const proc = x('nonexistentforsure');
const lines = [];
const lines: string[] = [];
for await (const line of proc) {
lines.push(line);
}
Expand Down

0 comments on commit f11e42b

Please sign in to comment.