Aditya Kumar Gupta
Aditya Kumar Gupta

Reputation: 107

How to suppress stack trace after `panic` in Go?

I am developing a CLI tool and in cases where the things go wrong, I want to log the custom error and exit with panic. The issue with panic is that an exit by panic is followed by the stack trace which I don't want to show to the user. Is there a way to panic and have a ninja-like stealthy/quiet exit?

(Choosing panic over os.Exit() since that would handle any defer and also seems a lot cleaner.)

Upvotes: 1

Views: 2225

Answers (1)

kostix
kostix

Reputation: 55463

Well, a direct answer is yes, there is a way:

  1. panic with an error of a custom, "sentinel", type, or with a custom, "sentinel", value, then
  2. Have a defer-red call in main which recover()-s the panic and checks whether the returned value is of a sentinel type (or equals to a sentinel value—the exact approach is up to you).
    If it detects a "known" error, it silently exits with a non-zero exit code; otherwise it panics again.

But honestly I think your mindset is too affected by programming languages with exceptions: I have yet to see a CLI app which would have any difficulty in handling errors "the usual way" or would actually benefit from bailing out using panic.

A counter-argument to the approach you're craving for is this: bubbling up an error allows adding more context to it on each level of the call stack being unwound, where it makes sense,—producing as useful as possible error to display. Basically, it works like this:

func main() {

  ...

  err := DoStuff()
  if err != nil {
    log.Fatal("failed to do stuff: ", err)
  }

  ...
}

func DoStuff() error {
  foo, err := InitializeWhatever()
  if err != nil {
    return fmt.Errorf("failed to inialize whatever: %w", err)
  }

  ...

  return nil
}

func InitializeWhatever() (*Whatever, error) {
  handle, err := OpenWhateverElse()
  if err != nil {
    return nil, fmt.Errorf("failed to open whatever else: %w", err)
  }

  ...

  return whatever, nil
}

…which would produce something like

failed to do stuff: failed to inialize whatever: failed to open whatever else: task failed successfully

…which makes it crystal clear which sequence of events led to the undesired outcome.

Sure, as usually, YMMV and no one except you knows your situation best, but still it's something to ponder.


And here's an assorted list of thoughts on what I've written above.

  • The approach with panicking with a known value / error of a known type is actually nothing new—for instance, see net/http.ErrAbortHandler.
    The encoding/json package used to employ this approach for breaking out of multiple nested loops (has been reworked since then, though).
  • Still, some experts consider sentinel errors to be bad design and recommend instead asserting (using Go's type assertions) behaviour of the errors—for instance, you can look at how net.Error has Timeout and Temporary methods which allows to not have concrete exported types for temporary errors, and errors due to timeouts, and their combinations,—but instead have them all support a common set of methods which allow the callers to make sense of the error's nature.
  • Since 1.13, Go gained advanced support for "wrapping" errors—producing matryoshka-style error chains, and this allows an approach to error handling which sits somewhere between the two extremes above: one can create an error of a particular type at the place an error was first found, then wrap it in contexts as they bubble up the call stack, and then assert the type of the innermost error at the place which handles the error. A friendly explanation on how it works.

Well, and I'd recommend reading this for, I must admit, had you already done this, you'd probably not ask your question in the first place ;-)

Upvotes: 4

Related Questions