Skip to content

Commit

Permalink
feat: better error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
hongaar committed May 27, 2020
1 parent 70d5b7f commit c5c7601
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 55 deletions.
92 changes: 70 additions & 22 deletions examples/errors.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,82 @@
import { program, command } from '../src'

const syncOk = command('sync', 'Print sync message').action(function () {
console.log('ok/sync')
})
const asyncOk = command('async', 'Print sync message').action(
async function () {
const app = program()

const syncOk = command('sync')
.description('Print sync message')
.action(function () {
console.log('ok/sync')
})

const asyncOk = command('async')
.description('Print sync message')
.action(async function () {
console.log('ok/async')
}
)
const syncNok = command('sync', 'Throw sync error').action(function () {
throw new Error('nok/sync')
})
const asyncNok = command('async', 'Throw async error').action(
async function () {
})

const syncNok = command('sync')
.description('Throw sync error')
.action(function () {
throw new Error('nok/sync')
})

const asyncNok = command('async')
.description('Throw async error')
.action(async function () {
throw new Error('nok/async')
}
})

const syncValidation = command('sync')
.description('Test validation error with sync handler')
.argument('required')
.action(function () {
console.log('call without arguments')
})

const asyncValidation = command('async')
.description('Test validation error with async handler')
.argument('required')
.action(async function () {
console.log('call without arguments')
})

const noHandler = command('no_handler').description(
'Test missing command handler'
)

// Say bye
const printBye = () => console.log('Bye!')
const success = (resolved: unknown) => console.log('[success]', resolved)

// Print error message only (omit stack trace) and exit with a meaningful status
const printError = (error: any) => {
console.error(String(error))
process.exit(42)
const fail = (error: any) => {
console.error('[failed]', String(error))

if (!app.isRepl()) {
process.exit(42)
}
}

program()
.add(command('ok', 'Print various messages').add(syncOk).add(asyncOk))
.add(command('nok', 'Throw various errors').add(syncNok).add(asyncNok))
app
.add(
command('ok')
.description('Print message to stdout from handler')
.add(syncOk)
.add(asyncOk)
)
.add(
command('nok')
.description('Throw various errors')
.add(syncNok)
.add(asyncNok)
)
.add(
command('validation')
.description('Validation errors')
.add(syncValidation)
.add(asyncValidation)
)
.add(noHandler)
.withHelp()
.runOrRepl()
.then(printBye)
.catch(printError)
.then(success)
.catch(fail)
12 changes: 12 additions & 0 deletions examples/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { program, command } from '../src'

const string = command('string')
.argument('arg', 'Required string argument')
.option('opt', 'Optional string option', { type: 'string' })
.action((args) => {
console.log('Args are', args)
})

const app = program('All argument and option types').add(string)

app.withHelp().runOrRepl()
2 changes: 1 addition & 1 deletion src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class Command<T = {}> {
* This is the base method for adding arguments, options and commands, but it
* doesn't provide type hints. Use `.argument()` and `.option()` instead.
*/
add(obj: Argument | Option | Command) {
add(obj: Argument | Option | Command<any>) {
if (isArgument(obj)) {
// If last argument is variadic, we should not add more arguments. See
// https://github.com/yargs/yargs/blob/master/docs/advanced.md#variadic-positional-arguments
Expand Down
24 changes: 9 additions & 15 deletions src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@ export class Program {
return extractCommandFromProcess().length ? this.run() : this.repl()
}

/**
* Returns `true` if program is running a repl loop, `false` otherwise.
*/
public isRepl() {
return !!this.replInstance
}

/**
* Method to execute when a failure occurs, rather than printing the failure
* message.
Expand All @@ -228,22 +235,9 @@ export class Program {
* instance originally thrown and yargs state when the failure occured.
*/
private failHandler(msg: string, err: Error, yargs: Argv) {
// TODO needs more use-cases: only do something when msg is set, and have
// errors always handled in the runner?

if (msg) {
// If msg is set, it's probably a validation error from yargs we want to
// print.
console.error(msg)

if (this.replInstance) {
// In case we're in a REPL session, indicate we printed a message, so we
// can prevent the program resolve handler to execute.
this.replInstance.gotYargsMsg()
} else {
// In other cases, exit with status of 1.
process.exit(1)
}
// Simply throw validation messages to reject runner promise
throw new Error(msg)
}
}
}
18 changes: 1 addition & 17 deletions src/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export class Repl {
private server?: REPLServer
private autocompleter: Autocompleter

private receivedYargsMsg: boolean = false
private successHandler: (value?: unknown) => void = () => {}
private errorHandler: (reason?: any) => void = (reason) =>
console.error(reason)
Expand All @@ -37,16 +36,6 @@ export class Repl {
})
}

/**
* Signal the REPL instance that a msg was generated by yargs when parsing the
* args. This method may change at any time, not intended for public use.
*
* @private
*/
public gotYargsMsg() {
this.receivedYargsMsg = true
}

/**
* Emulates promise.then, but saves the callback instead to be executed on
* each command which resolves.
Expand Down Expand Up @@ -97,14 +86,9 @@ export class Repl {
file: string,
cb: (err: Error | null, result: any) => void
) {
this.receivedYargsMsg = false

try {
const result = await this.program.run(line.trim())

// Execute success handler only if we have not received a msg from yargs,
// which was probably a validation error.
this.receivedYargsMsg || this.successHandler(result)
this.successHandler(result)
} catch (error) {
this.errorHandler(error)
}
Expand Down

0 comments on commit c5c7601

Please sign in to comment.