DEV Community

Cover image for Don't panic!
Jonathan Hall
Jonathan Hall

Posted on • Originally published at boldlygo.tech

Don't panic!

#go

This post comes from my daily email list: Boldly Go! Daily. Sign up to learn a bit more about Go every day!

--

Don’t panic.

Don’t panic is such ubiquitous Go advice that it’s one of the famous Go proverbs.

So why do we have a built-in panic function if we’re not supposed to use it?

Well, it’s not so much that it’s never appropriate to use, as much as it’s often heavily overused. Especially by programmers who are accustomed to using exceptions and try/catch blocks to manage control flow.

Don’t do that!

At the moment I’m helping one client with a massive refactor of their application to remove the problematic, non-idiomatic pattern of using panic rather than proper Go error handling.

I’m sure we’ll talk a lot more about this in the future, but probably the most important reason not to panic is that it’s very non-obvious. This problem exists in languages that uses exceptions for error handling, too (and is why Go’s design doesn’t encourage this pattern). Consider this code (taken from the above mentioned client’s code base, with names edited to protect the innocent):

func CreateRecordWithIDAndType(ctx context.Context, id model.ID, typ model.ResourceType) {
    userID := appctx.AuthenticatedUserIdOptional(ctx)
    if userID.Valid() {
        recordReq := &RecordRequest{
            ActionRequest: ActionRequest{
                Type: learnType,
                Id:   id,
            },
        }

        CreateRecordWithCDP(ctx, recordReq)
    }
}
Enter fullscreen mode Exit fullscreen mode

Here’s the question: Can the call to CreateRecordWithCDP result in an error condition?

It’s impossible to be sure. Although we have one strong clue: It accepts a context.Context value, which is often, though not always, an indication that the function call may block, and be terminated early if the context is cancelled. This would suggest that an error state is possible. Although the context could just be used to read a value, in which case it may legitimately not be possible for CreateRecordWithCDP to result in an error condition. It also doesn’t return anything, so whatever it’s “creating”, presumably is stored somewhere–probably in a database. And database operations, as a rule, can err.

In any case, since we don’t know, we’re forced to either investigate CreateRecordWithCDP (and any functions it calls) to determine if it might panic, or just play it safe, and add a deferred recover before we call it.

Wouldn’t it be much easier if we had some obvious indication? Perhaps like this new version?

func CreateRecordWithIDAndType(ctx context.Context, id model.ID, typ model.ResourceType) error {
    userID := appctx.AuthenticatedUserIdOptional(ctx)
    if userID.Valid() {
        recordReq := &RecordRequest{
            ActionRequest: ActionRequest{
                Type: learnType,
                Id:   id,
            },
        }

        return CreateRecordWithCDP(ctx, recordReq)
    }
  return nil
}
Enter fullscreen mode Exit fullscreen mode

This make the fact that calling CreateRecordWithCDP (and in turn CreateRecordWithIDAndType) can result in an error condition. And it makes programming to use these functions much simpler, and eliminates a lot of guesswork.

Of course, this does lead to the situation many complain about, of cluttering your code with if err != nil { ... }. But IMO, while this is a bit annoying to type, typing is a very small price to pay for the clarity it provides.

Top comments (0)