5.6.1 Exit Code Taxonomy

An exit code is a classification, not a diagnosis. It tells a CI wrapper which kind of outcome Bazel reported. The log, Build Event Protocol data, and the failed action still explain the cause. At consultant depth, the important skill is to preserve that distinction and avoid turning a convenient number range into an unsafe retry policy.

1.1.9 Exit Codes introduced the everyday command outcomes, while 3.6.3 CI-Specific Flags & Exit Codes put them into a basic CI handler. Here is the complete public taxonomy documented by Bazel today.1

Codes shared by Bazel commands

CodeBazel's classificationOperational interpretation
0SuccessAccept the invocation.
2Command-line problemFix flags, their combination, or relevant environment variables. Repeating the same invocation is not a recovery strategy.
8Interrupted, with orderly shutdownDetermine who or what canceled the invocation before deciding whether to retry.
9Server lock held while --noblock_for_lock was setEither wait, serialize callers, or give concurrent jobs distinct output bases.
32External environment failure, not on this machineInspect the detailed failure. The number alone does not prove that a retry will succeed.
33Bazel ran out of memory and crashedPreserve diagnostics and change resources, scope, or JVM settings before retrying.
34, 35Reserved for Google-internal useDo not assign local meanings to reserved codes.
36Local environmental issue, suspected permanentRepair the local environment rather than blindly retrying.
37Unhandled exception or internal Bazel errorPreserve the server log and invocation evidence. Retry only under an explicit, bounded policy.
38Transient Build Event Service publishing errorA bounded retry may be reasonable if publishing is required, but the build outputs may already exist.
39Required blobs were evicted from a remote cacheRetry can let Bazel recover or rebuild, subject to the cache and remote-execution policy.
4144Reserved for Google-internal useTreat as unknown non-zero results in portable tooling.
45Persistent Build Event Service publishing errorFix the service or configuration. Do not classify it as transient merely because it is infrastructure-related.
47, 49Reserved for Google-internal useTreat as unknown non-zero results in portable tooling.

The table deliberately does not collapse 32 through 45 into one “environment failure” range. That interval contains reserved values, a Bazel OOM, a suspected-permanent local problem, an internal error, one explicitly transient BES failure, and one explicitly persistent BES failure. Numeric proximity is not a semantic contract.1

Command-specific meanings

Interpret the command before interpreting the number:

CommandCodeMeaning
build or test1The build failed.
test3The build succeeded, but one or more tests failed or timed out.
test4The build succeeded, but testing was requested and no tests were found.
run1The build failed.
runnon-zero from the launched programAfter a successful build, Bazel returns the executed subprocess's non-zero status.
query3Partial success: results were produced despite input BUILD-file errors, commonly with --keep_going, so the answer is incomplete.
query7The query command failed.

This context prevents two common mistakes. Code 3 is a test outcome for bazel test but an incomplete graph answer for bazel query. With bazel run, an application can return a number that also appears in Bazel's taxonomy. A wrapper that sees only 37, for example, cannot infer “internal Bazel error” without also knowing whether Bazel completed the build and launched the program.1

Build a conservative CI classifier

Capture the status immediately, record the command kind, and retain the log before running another Bazel command. bazel info command_log identifies the log location, but invoking bazel info itself replaces the most recent command log, so automation should resolve that stable path in advance or copy the log before issuing another Bazel command.1

bazel test --config=ci //...
status=$?

case "$status" in
  0) outcome=success ;;
  1) outcome=build_failure ;;
  3) outcome=test_failure ;;
  4) outcome=no_tests ;;
  9) outcome=lock_contention ;;
  38|39) outcome=retry_candidate ;;
  *) outcome=needs_triage ;;
esac

printf '%s\n' "bazel_outcome=$outcome" "bazel_exit_code=$status"
exit "$status"

This is intentionally an allowlist, not if status >= 32. Even 38 and 39 should be retried only a bounded number of times and only when the surrounding system makes the retry useful. Code 9 usually calls for serialization or a separate --output_base, not repeated collision with the same lock. Code 33 calls for preserving OOM evidence and changing conditions, not replaying the same memory demand.1

The wrapper should also have a safe default for codes it does not recognize. Bazel explicitly reserves the right to add more specific non-zero codes in the future, while preserving the guarantee that every non-zero result is an error. Fail closed, retain the raw number and diagnostic artifacts, and update policy only after a new code has a verified meaning.1

That classification is one input to 6.6.5 Classifying Failures Before Retrying, which combines the status with attempt, service, worker, and shared-state evidence before choosing retry, repair, quarantine, or rollback. Exit status can nominate a retry candidate. It cannot prove that an outage is transient, that replay is idempotent, or that the same resource limits will be sufficient.

key takeaway

Read a Bazel exit status together with the command that produced it. Use the documented exact meanings, preserve unknown non-zero values, and build CI retry logic from a small reviewed allowlist rather than a numeric range. The code routes triage. Retained diagnostics establish the cause.

Check your understanding · 3 questions

1.Two CI jobs both return exit code 3. One ran bazel test. The other ran bazel query --keep_going. How should the wrapper classify them?

Select one answer

2.After a successful build, bazel run //app launches the application and exits with code 37. What is the safest interpretation?

Select one answer

3.A team is designing a conservative retry policy. Which responses are safe starting points?

Select all that apply

0 of 3 answered

Footnotes

  1. Calling Bazel from scripts — current common and command-specific exit-code tables, bazel run subprocess passthrough, future-code compatibility warning, output-base locking, and command_log behavior 1 2 3 4 5 6