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
| Code | Bazel's classification | Operational interpretation |
|---|---|---|
0 | Success | Accept the invocation. |
2 | Command-line problem | Fix flags, their combination, or relevant environment variables. Repeating the same invocation is not a recovery strategy. |
8 | Interrupted, with orderly shutdown | Determine who or what canceled the invocation before deciding whether to retry. |
9 | Server lock held while --noblock_for_lock was set | Either wait, serialize callers, or give concurrent jobs distinct output bases. |
32 | External environment failure, not on this machine | Inspect the detailed failure. The number alone does not prove that a retry will succeed. |
33 | Bazel ran out of memory and crashed | Preserve diagnostics and change resources, scope, or JVM settings before retrying. |
34, 35 | Reserved for Google-internal use | Do not assign local meanings to reserved codes. |
36 | Local environmental issue, suspected permanent | Repair the local environment rather than blindly retrying. |
37 | Unhandled exception or internal Bazel error | Preserve the server log and invocation evidence. Retry only under an explicit, bounded policy. |
38 | Transient Build Event Service publishing error | A bounded retry may be reasonable if publishing is required, but the build outputs may already exist. |
39 | Required blobs were evicted from a remote cache | Retry can let Bazel recover or rebuild, subject to the cache and remote-execution policy. |
41–44 | Reserved for Google-internal use | Treat as unknown non-zero results in portable tooling. |
45 | Persistent Build Event Service publishing error | Fix the service or configuration. Do not classify it as transient merely because it is infrastructure-related. |
47, 49 | Reserved for Google-internal use | Treat 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:
| Command | Code | Meaning |
|---|---|---|
build or test | 1 | The build failed. |
test | 3 | The build succeeded, but one or more tests failed or timed out. |
test | 4 | The build succeeded, but testing was requested and no tests were found. |
run | 1 | The build failed. |
run | non-zero from the launched program | After a successful build, Bazel returns the executed subprocess's non-zero status. |
query | 3 | Partial success: results were produced despite input BUILD-file errors, commonly with --keep_going, so the answer is incomplete. |
query | 7 | The 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.
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