-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Best practice for error output? #63
Comments
I think your 3 error scenarios are valid and well summarized. Sywac attempts to handle number 1 and number 3, but it doesn't really handle number 2. If your program code throws an error for number 2, then sywac handles it as number 3. If your program code uses In my CLI programs, I guess I try to avoid number 2, but when I've needed it, I'm typically content to treat it as number 1 or number 3. There have been times when I've let the command handler do its own error logging and program short-circuiting, but that's probably not the best way to do it, especially if (a) the program needs to do some resource cleanup after a command was attempted or (b) the program is a server-side chatbot. We could include a special |
I should note, however, that you can use |
It's also possible to just |
Ah, I just looked into that. It looks like that would be useful in simple inline commands (where you have access to sywac's context). |
This is actually what I'm angling for although full disclosure, I'm not sure if it's even a good idea. Typically, I'm working in a CLI app with several commands that all call various sub modules and each of these sub modules could have pretty deeply nested errors that are still "user-fixable" where a stack trace is not desirable. I guess an example is something like: const HelpError = require('../errors').HelpError;
// in a module 3 levels divorced from the CLI
throw new UsageError([
'It looks like your git config is not set properly. Run the following command and try again:'
'',
' git config --global acme.user.apitoken <your api token>',
''
].join('\n')); In this case it's "worth it" to create an Error class used by several modules that denotes "don't show a stack with this one". If sywac included such an Error, that would be nice but now a lot of "pure" (non-CLI) modules, that have a specific purpose, need to include a command-line module: const StacklessError = require('sywac').StacklessError; (I realize me calling my module "pure" is kind of laughable considering the clearly CLI-formatted advice it is throwing. I guess to be truly "pure", my modules should be throwing a Other ideas I've thrown around are goofy string-parsing things, like prefixing all of my "helpful text" errors with a string, and using that as a marker. E.g.: throw new Error(':: Aborted, try again.'); |
I think what I'd want most from sywac is not to dictate how I tell the difference between errors in Camp 2 and Camp 3, but just to let me do it without resorting to In other words, a tidbit like this: sywac
.command({...})
.errorFormatting(err => err.message.startsWith('::') ? err.message : err.stack)
.parseAndExit() Now I can be as clever or as stupid in my own CLI program as I want, but sywac still lets me print errors in all 3 categories using EDIT: Actually I'm not 100% sure that I can distinguish between Error 1 and Error 3 if I'm specifying |
Yeah, sounds like it would be best to allow the CLI author to tell sywac one (or more) of the following:
Or, as you suggested, this is the "style" I want you to use when formatting a caught error. I'd be willing to support any or all of these ideas. |
I'll noodle on it too! The fly in the ointment is the non-command API which would usually be parseAndExit chained into a then block with your main function. In this case, Camp 2+3 errors are out of sywac's hands (it will only handle Camp 1). I think in my ideal world, not only would we add some form of synchronous configuration you mention above, but also an asynchronous re-entry point for handling errors: sywac
.string(...)
.boolean(...)
.parseAndExit()
.then(argv => {
// my simple program
})
.catch(sywac.handleErrors) This way even this type of program could automatically have sywac use your style's messages style for the error, for example. |
OK, I've been thinking about this, and I have an idea - let me know what you think. One of the reasons I became really attracted to sywac in the first place is that when using commands, there is a great symmetry for unit testing. You can define an entry point like this: // src/cli.js
module.exports = require('sywac')
.command(blah)
.command(blah)
.strict()
.showHelpByDefault(); Now that you've defined your entire program, you can write your binary: // bin/myprogram
#!/usr/bin/env node
require('../src/cli').parseAndExit(); And your unit test: // spec/cli-spec.js
it('should work', function () {
const result = cli.parse('some arguments');
// various expectations on the .code & .output of your program
}); This beautiful symmetry becomes broken, unfortunately, if you are NOT using commands. Your code doesn't get hit until after the parse/parseAndExit call is complete, which means you cannot structure your program exactly this way - you need an additional wrapper. My proposal is to make the non-command version more like the command version by allowing (probably optionally?) the user to "define" their program as a root-level run command. /* Instead of this */
require('sywac')
.string('foo')
.parseAndExit()
.then(argv => /* my program */);
/* Let's allow this */
require('sywac')
.string('foo')
.run(argv => /* my program */)
.parseAndExit(); This allows you to retain that "either execute, or test" symmetry that I love whether you are using commands or not, AND, it let's a new error formatter (which I'm tentatively thinking will end up being What do you think? |
For most of my non-command CLIs, I try to organize my package into two main parts - one is a library module intended to be used via In that type of structure, I typically write tests to cover the primary use-cases by executing the CLI in a separate process (using When comparing this approach to what you describe, the crux seems to be the necessity to execute the actual bin/executable in a separate process for testing purposes (which, quite honestly, is sometimes a pain in the butt). I agree that it would be nice to just I've always viewed testing the actual bin/executable module as a necessary evil, and I'd love to come up with a standard way to make this super easy (though I don't know of a way to do this without writing a separate opinionated package), but I think both of these different approaches (the one you describe and the one I describe) are valid and don't need to be mutually exclusive when it comes to being supported by sywac. So, long story short, I'd like to explore your idea of adding something like Thanks for the great feedback! I think you've got some really good ideas. |
Thanks much! Re: testing the bin file: if I were to write a "Testing Guide" for sywac.io someday, what I'd want to recommend to people is kind of a hybrid:
|
Yeah, that makes sense. Using in-process tests gives you a higher degree of flexibility and control over the testing environment (e.g. injection), so I would prefer to use that (instead of executing a CLI via child process) whenever possible. |
An interesting aside I discovered today: you actually could get exactly what I want, with no changes to sywac: class StacklessError extends Error {
constructor(message) {
super(message)
this.name = ''
this.stack = ''
}
} Because of how Error's throw new StacklessError('Hello user, please try again later.');
// => Hello user, please try again later. Suggesting that the user create their own copy of this "StacklessError" and throw it, if they'd like sywac to print a stackless error on their behalf, is another possible strategy. |
Concerning your last comment about defining a That's an interesting approach, but I wouldn't recommend that developers override basic Error semantics (e.g. throwing away the |
I find myself repeating a pattern with most of my CLI programs, curious if there is an alternate take.
Usually there are 3 classes of CLI errors:
Argument/command line validation error - in this case, I probably want to detect it early, in
check()
, and generate usage details + error viacliMessage
.Expected error cases - in this case, I don't want usage help, but would like to print an error message with no stack trace.
Unexpected error cases - an exception (from my code or a library) bubbled up, in this case I want a full stack trace for debugging.
Here's a complete example:
(The
parseAndExit
nicely handles the last part for you, but you can't insert your custom error class handling above if you use that.)I'm curious if this tracks for you, creating a token "error" class in every CLI program so you can distinguish between expected and unexpected errors. I've created this token error class in so many different utilities and libraries that I sometimes wish the concept of a
UserError
- a thrown Error containing a message intended for the user - existed in core nodejs, and then sywac could handle it for me by printing the.message
if it's a UserError or the.stack
if it's not.The text was updated successfully, but these errors were encountered: