skip to content

Your team owns a Go library other teams import. How do you decide where panics are allowed in it?

level: principalimportance: nice to knowfreq 33%

answer

  1. the signature does not show a panic
  2. one strict default, a closed exception list
  3. the reviewer owns the exception
  4. ask where the argument came from
  5. strictness scales with blast radius

basics

~20 s

Set one strict default: no exported function panics on caller-controlled data. Allow a short, closed list of exceptions, such as package-level Must calls on source literals. Make the reviewer own exceptions, and back the rule with adversarial tests.

solid answer

~50 s

A panic on an exported path is a behavioural contract the signature does not show and every importer inherits, so it is an API-ownership decision, not a style preference. I write down one strict default — nothing exported panics on a value the caller controls — and a short closed list of exceptions: package-level `Must` calls on source literals, a programmer-error contract stated in the doc comment, and internal invariants in unexported code. Exceptions are the reviewer's call, not the author's, and the question that catches most regressions is "where did this argument come from?" — the usual defect is a `MustCompile` moved out of a package-level var into a function that takes a parameter. I back the rule with adversarial tests and fuzz targets so a new panic fails a test, not a deploy. I refuse a blanket recovery inside the library: whether to survive a bug is the importing application's decision.

go deeper

for a junior

Take away the direction of the rule: code other people import is held to a stricter standard than a program you run yourself, because the crash lands in their process.

for a middle

Be able to name the sanctioned exceptions and why each is safe, especially why a package-level Must on a literal fails at startup rather than during a request.

for a senior

Show how you enforce it in practice: what a reviewer asks about a new Must call, and which tests make a newly introduced panic fail before release.

for a principal

Own the whole policy: the closed exception list, who can grant one, the migration cost of removing a released panic, and why a library must not make the recovery decision for its importers.

### What the policy actually decides "May this package panic?" looks like a coding-style question and is not. For a package other teams import, a panic on an exported path is a **behavioural contract that the type signature does not show**. Your importers cannot see it when they read `func (r *Rule) Apply(...) (bool, error)`, they inherit it in their own process, and in a server it typically becomes a restart rather than a failed request. That makes the decision an API-ownership call: the author proposes, the API reviewer or the consuming team can overrule, and once the package is out in the world the decision is expensive to reverse. ### The default rule to write down Start from a rule strict enough that it can be applied in review without argument: > No exported function in this package panics on a value the caller controls. Then enumerate the exceptions — a short, closed list, not a principle: 1. **Package-level `var` and `func init` on literals.** `regexp.MustCompile` on a pattern written in the source file, an embedded template that must parse. The failure is a build bug and it surfaces at process start, in tests and CI, before anything serves. 2. **A documented programmer-error contract**, such as using a type without the constructor the package requires. It must be in the doc comment on the exported name, or it does not count. 3. **Internal invariants**, in unexported code, whose message names the bug. Everything else — configuration, request data, file contents, environment — returns an error. ### Who owns the exceptions The useful organisational move is to make the exception list owned by the reviewer rather than the author. An author adding a `Must` call inside a function argues for it in the pull request; the reviewer's job is one question: *where did this argument come from?* That single question catches the most common regression in this area, which is not someone writing a reckless new panic but someone **moving** an existing, correct `MustCompile` out of a package-level variable and into a function so it can take a parameter. The diff looks like a refactor; the crash moved from startup into the request path. ### Enforcement that does not rely on vigilance Review alone decays. Back it with mechanism: - Adversarial tests over every exported function — zero values, nil maps, nil slices, wrong dynamic types — so a new panic fails a test rather than a deployment. - Fuzz targets for anything that parses caller bytes or strings. - A convention that `Must`-style helpers live only in `_test.go` files and in package-level declarations, which is a grep-able rule. - Package documentation that states the policy, so importers can rely on it and file a bug when it is broken. ### The tradeoffs to say out loud Being strict is not free, and a principal answer should price it: - **Plumbing.** Every rejected panic becomes an error return threaded through call sites that had nothing to say about it. Sometimes the right response is to restructure so validation happens once at the boundary rather than to thread an error through six layers. - **Convenience lost.** `Must` genuinely makes tests and static declarations pleasant. Keeping it legal in exactly those places preserves most of the benefit. - **The blanket-recover temptation.** Somebody will propose wrapping every exported entry point in a deferred recovery so "the library can never crash the caller". Refuse it for a library: it converts a bug into a wrong answer, hides the defect from the importer, and takes a decision that belongs to the process owner. Whether to install a last-resort recovery is the *application's* call, not the library's. - **Migration cost.** Removing a panic from a released exported function is a behaviour change for every importer. Where the fix needs a new signature, the pragmatic path is a new error-returning function alongside the old one, documented as the replacement, rather than a silent change of behaviour under the same name. ### Calibrating strictness to distance A final principle that makes the policy explainable: **the further your code is from the process that owns the crash, the less right it has to panic.** A `main` package may stop; a widely imported library may not. An internal tool with three known callers can be looser than a module a dozen teams depend on. State the rule per package, in its doc comment, and let the blast radius set the strictness rather than personal taste.

  • A team proposes wrapping every exported entry point in a deferred recovery so the library can never crash an importer. What is your answer?
    No, for a library. It converts a bug into a wrong answer, hides the defect from the importer, and takes a decision that belongs to the process owner. An application may install a last-resort net at its own boundary; a library must not make that choice on its behalf.
  • How would you remove a panic from an exported function that other teams already depend on?
    Treat it as a behaviour change, not a cleanup. If the replacement needs a different signature, add an error-returning function alongside the old one and document it as the replacement, so importers migrate on their own schedule rather than discovering a silent change under the same name.
  • Does the same policy apply to an internal tool with three known callers?
    It scales with blast radius. With three callers you can be looser, because a behaviour change is a conversation rather than a release. State the rule per package in its doc comment so the strictness is visible, rather than leaving it to whoever wrote it most recently.
  • How do you keep the policy alive once review attention drifts?
    Mechanism, not vigilance: adversarial table tests over zero values for every exported function, fuzz targets for anything parsing input, a grep-able convention that Must helpers appear only in package-level declarations and test files, and the guarantee written into the package documentation so importers can file a bug when it breaks.

saying these in an interview costs you the question

  • Leaves the rule to individual judgment with nothing written down
  • Adds a blanket recover in the library so it can never crash callers
  • Applies the same strictness regardless of how many teams import it
  • Treats removing an exported panic as a harmless internal cleanup
  • Bans Must helpers everywhere, including package-level vars and tests