Don't panic!

March 28, 2024

Later today, at 15:00 UTC, I’ll be joining Denis Čahuk and Adrian Stanek on their regular Livestream, Our Tech Journey, to talk about TDD, Go, and do some live coding. Join us!


The second in our list of terminating statements is… panic!

Terminating statements

  1. A call to the built-in function panic.

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)
	}
}

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.

And even if we do that, any callers of CreateRecordWithIDAndType are stuck with the exact same situation, and have to do some deep investigation, or add their own recover.

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
}

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.

Quotes from The Go Programming Language Specification Language version go1.22 (Feb 6, 2024)


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Get daily content like this in your inbox!

Subscribe