-
Notifications
You must be signed in to change notification settings - Fork 13
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
User-facing errors in example runner #2741
Conversation
MDrakos
commented
Sep 1, 2023
•
edited by github-actions
bot
Loading
edited by github-actions
bot
DX-2151 User facing errors for example runner |
func (h *Hello) Run(params *RunParams) error { | ||
err := h.run(params) | ||
if err != nil { | ||
for _, unpackedError := range errs.Unpack(err) { | ||
switch unpackedError.(type) { | ||
case *runbits.NoNameProvidedError: | ||
// Errors that we are looking for should be wrapped in a user-facing error. | ||
// Ensure we wrap the top-level error returned from the runner and not | ||
// the unpacked error that we are inspecting. | ||
return &HelloUserFacingError{ | ||
locale.WrapInputError( | ||
err, | ||
"hello_err_no_name", | ||
"Cannot say hello because no name was provided.", | ||
), | ||
} | ||
|
||
default: | ||
// If this is not a specific error we are looking for, do nothing. | ||
continue | ||
} | ||
} | ||
|
||
return locale.WrapError(err, "hello_cannot_say", "Cannot say hello.") | ||
} | ||
|
||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the approach of having a Run calling another run, where the first one is basically a post-processor of the latter. That feels like an inverse of responsibilities in how you traverse the code when reading.
I think we should have a single Run function, and we defer the error "post processor". So at the start of the run function we'd have something like
func processError(err *error) {
...
*err = errs.WrapUserFacing(err, "You broke it!")
...
}
func (h *Hello) Run(params *RunParams) (rerr error) {
defer processError(&rerr)
...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer the approach that is currently in the PR. While having a function that does post-processing + defer
fits nicely in a mental model in practice I find that when reading the code it requires more jumping around, or at least remembering that you have to jump to the defer function after reading the runner logic. This isn't a problem with the current code as you read the Run
function, then the function that it calls, and return to the caller.
Post-processing with a defer
also requires a named return value which is different from the vast majority of the functions in our codebase and also adds another layer of complexity when reading the code.
This is all subjective and in the end, it's your call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern of the named return is not uncommon. It is used frequently in our codebase.
I'm open to other solutions, but between the current implementation and what I am suggesting I'd prefer my suggestion. Giving each runner a redundant Run()
function that you have to interpret to understand does not feel like the better solution to me, but you're right, that is subjective. But, given this is meant as a reference for our codebase rather than a one-off piece of code I'm gonna be nitpicky and insist on either my solution or an alternate yet to be determined one.
pkg/cmdlets/errors/errors.go
Outdated
@@ -93,8 +93,6 @@ func ParseUserFacing(err error) (int, error) { | |||
return 0, nil | |||
} | |||
|
|||
_, hasMarshaller := err.(output.Marshaller) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is breaking the original intend of the function. Just way predated the new design and isn't looking to address the same use-case. What you effectively found here is that the term "User Facing" was already in use, and we need to either resolve the conflict of overlapping terms, or ensure that they are compatible.
Looking through the code it seems hasMarshaller
will mainly identify whether we already have an OutputError on our hands, so we don't redundantly wrap it. While I don't see any scenario's in which this could be true, do we want to risk breaking this behaviour for this PR?
Please take some time to understand what OutputError{} is doing and how the new concept of user facing errors plays into this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I can grok the OutputError
type adds extra post-processing and the handling in this function ensures that errors returned all have a similar UX. I've wrapped the user-facing error in this OutputError
type as well to ensure it's consistent with all of the other errors we are presenting to users.
I've also added back the hasMarshaller
check as you were right, we don't want to wrap the error again in the OutputError
type.
Adjust how we handle errors in example runner Add back marshaller handling in user facing error parsing
pkg/cmdlets/errors/errors.go
Outdated
uerr, ok := errs.IsUserFacing(err) | ||
if ok { | ||
logging.Debug("Returning user facing error, error stack: \n%s", errs.JoinMessage(err)) | ||
return code, &OutputError{uerr} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think OutputError produces the error string from UserFacingError()
?
Please make sure you run this code to ensure it produces the error message you expect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does here: https://github.com/ActiveState/cli/blob/master/pkg/cmdlets/errors/errors.go. I've confirmed this from running the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand, you linked to code on master which definitely would not be handling this new concept. Wrong link?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, yes wrong link.
I've run the code and confirmed the output from UserError()
is there. I believe it's done via the output mediator here: https://github.com/ActiveState/cli/blob/DX-2151/internal/output/mediator.go#L54
Add back interface Remove unused error type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest we try to do some pair programming tomorrow to resolve this PR faster.
internal/errs/errs.go
Outdated
} | ||
|
||
func (e *userFacingError) Error() string { | ||
return JoinMessage(Wrap(e.wrapped, e.message)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return JoinMessage(Wrap(e.wrapped, e.message)) | |
return "User Facing Error: " + e.UserError() |
Update error tip handling
type NoNameProvidedError struct { | ||
*errs.WrapperError | ||
} | ||
|
||
func SayHello(out output.Outputer, name string) error { | ||
if name == "" { | ||
// Errors that are due to USER input should use `NewInputError` or `WrapInputError` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.