Collect and relay detailed failures in TargetEdgeErrorObservers

Annotates (non-exhaustively) non-exceptional failures during package
creation and evaluation, and surfaces those failures during query
evaluation, to be included in its detailed failure reporting.

Some rule-specific failures remain undetailed, for now, subject to
follow-up work.

RELNOTES: None.
PiperOrigin-RevId: 331586901
diff --git a/src/main/java/com/google/devtools/build/lib/events/Event.java b/src/main/java/com/google/devtools/build/lib/events/Event.java
index 18f9c5d..9d764ed 100644
--- a/src/main/java/com/google/devtools/build/lib/events/Event.java
+++ b/src/main/java/com/google/devtools/build/lib/events/Event.java
@@ -28,6 +28,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Function;
 import java.util.stream.Stream;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
@@ -411,6 +412,22 @@
   }
 
   /**
+   * Converts a list of {@link SyntaxError}s to events, each with a specified property, and replays
+   * them on {@code handler}.
+   */
+  public static <T> void replayEventsOn(
+      EventHandler handler,
+      List<SyntaxError> errors,
+      Class<T> propertyType,
+      Function<SyntaxError, T> toProperty) {
+    for (SyntaxError error : errors) {
+      handler.handle(
+          Event.error(error.location(), error.message())
+              .withProperty(propertyType, toProperty.apply(error)));
+    }
+  }
+
+  /**
    * Returns a {@link StarlarkThread.PrintHandler} that sends {@link EventKind#DEBUG} events to the
    * provided {@link EventHandler}.
    */
diff --git a/src/main/java/com/google/devtools/build/lib/packages/EnvironmentGroup.java b/src/main/java/com/google/devtools/build/lib/packages/EnvironmentGroup.java
index 21cbb7a..587d626 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/EnvironmentGroup.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/EnvironmentGroup.java
@@ -25,6 +25,7 @@
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code;
 import com.google.devtools.build.lib.syntax.Location;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -129,16 +130,22 @@
     for (Label environment :
         Iterables.filter(environmentLabels.environments, new DifferentPackage(containingPackage))) {
       events.add(
-          Event.error(
+          Package.error(
               location,
-              environment + " is not in the same package as group " + environmentLabels.label));
+              environment + " is not in the same package as group " + environmentLabels.label,
+              Code.ENVIRONMENT_IN_DIFFERENT_PACKAGE));
     }
 
     // The defaults must be a subset of the member environments.
     for (Label unknownDefault :
         Sets.difference(environmentLabels.defaults, environmentLabels.environments)) {
-      events.add(Event.error(location, "default " + unknownDefault + " is not a "
-          + "declared environment for group " + getLabel()));
+      events.add(
+          Package.error(
+              location,
+              String.format(
+                  "default %s is not a declared environment for group %s",
+                  unknownDefault, getLabel()),
+              Code.DEFAULT_ENVIRONMENT_UNDECLARED));
     }
 
     return events;
@@ -213,13 +220,25 @@
 
   private boolean isValidEnvironment(Target env, Label envName, String prefix, List<Event> events) {
     if (env == null) {
-      events.add(Event.error(location, prefix + "environment " + envName + " does not exist"));
+      events.add(
+          Package.error(
+              location,
+              prefix + "environment " + envName + " does not exist",
+              Code.ENVIRONMENT_DOES_NOT_EXIST));
       return false;
     } else if (!env.getTargetKind().equals("environment rule")) {
-      events.add(Event.error(location, prefix + env.getLabel() + " is not a valid environment"));
+      events.add(
+          Package.error(
+              location,
+              prefix + env.getLabel() + " is not a valid environment",
+              Code.ENVIRONMENT_INVALID));
       return false;
     } else if (!environmentLabels.environments.contains(env.getLabel())) {
-      events.add(Event.error(location, prefix + env.getLabel() + " is not a member of this group"));
+      events.add(
+          Package.error(
+              location,
+              prefix + env.getLabel() + " is not a member of this group",
+              Code.ENVIRONMENT_NOT_IN_GROUP));
       return false;
     }
     return true;
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Package.java b/src/main/java/com/google/devtools/build/lib/packages/Package.java
index 764edef..20c8635 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Package.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Package.java
@@ -36,11 +36,15 @@
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.EventKind;
 import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable;
 import com.google.devtools.build.lib.packages.License.DistributionType;
 import com.google.devtools.build.lib.packages.Package.Builder.PackageSettings;
 import com.google.devtools.build.lib.packages.RuleClass.Builder.ThirdPartyLicenseExistencePolicy;
 import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.PackageLoading;
+import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code;
 import com.google.devtools.build.lib.skyframe.serialization.DeserializationContext;
 import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec;
 import com.google.devtools.build.lib.skyframe.serialization.SerializationContext;
@@ -49,6 +53,7 @@
 import com.google.devtools.build.lib.syntax.Module;
 import com.google.devtools.build.lib.syntax.StarlarkSemantics;
 import com.google.devtools.build.lib.syntax.StarlarkThread;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Root;
@@ -172,6 +177,12 @@
    */
   private boolean containsErrors;
 
+  /**
+   * The first detailed error encountered during this package's construction and evaluation, or
+   * {@code null} if there were no such errors or all its errors lacked details.
+   */
+  @Nullable private FailureDetail failureDetail;
+
   /** The list of transitive closure of the Starlark file dependencies. */
   private ImmutableList<Label> starlarkFileDependencies;
 
@@ -432,6 +443,7 @@
     }
     this.buildFile = builder.buildFile;
     this.containsErrors = builder.containsErrors;
+    this.failureDetail = builder.getFailureDetail();
     this.starlarkFileDependencies = builder.starlarkFileDependencies;
     this.defaultLicense = builder.defaultLicense;
     this.defaultDistributionSet = builder.defaultDistributionSet;
@@ -530,6 +542,15 @@
     return containsErrors;
   }
 
+  /**
+   * Returns the first {@link FailureDetail} describing one of the package's errors, or {@code null}
+   * if it has no errors or all its errors lack details.
+   */
+  @Nullable
+  public FailureDetail getFailureDetail() {
+    return failureDetail;
+  }
+
   /** Returns an (immutable, ordered) view of all the targets belonging to this package. */
   public ImmutableSortedKeyMap<String, Target> getTargets() {
     return targets;
@@ -781,6 +802,24 @@
   }
 
   /**
+   * Returns an error {@link Event} with {@link Location} and {@link DetailedExitCode} properties.
+   */
+  public static Event error(Location location, String message, Code code) {
+    Event error = Event.error(location, message);
+    // The DetailedExitCode's message is the base event's toString because that string nicely
+    // includes the location value.
+    return error.withProperty(DetailedExitCode.class, createDetailedCode(error.toString(), code));
+  }
+
+  public static DetailedExitCode createDetailedCode(String errorMessage, Code code) {
+    return DetailedExitCode.of(
+        FailureDetail.newBuilder()
+            .setMessage(errorMessage)
+            .setPackageLoading(PackageLoading.newBuilder().setCode(code))
+            .build());
+  }
+
+  /**
    * A builder for {@link Package} objects. Only intended to be used by {@link PackageFactory} and
    * {@link com.google.devtools.build.lib.skyframe.PackageFunction}.
    */
@@ -858,7 +897,19 @@
     private final List<Postable> posts = Lists.newArrayList();
     @Nullable private String ioExceptionMessage = null;
     @Nullable private IOException ioException = null;
+    @Nullable private DetailedExitCode ioExceptionDetailedExitCode = null;
     private boolean containsErrors = false;
+    // A package's FailureDetail field derives from its Builder's events. During package
+    // deserialization, those events are unavailable, because those events aren't serialized [*].
+    // Its FailureDetail value is serialized, however. During deserialization, that value is
+    // assigned here, so that it can be assigned to the deserialized package.
+    //
+    // Likewise, during workspace part assembly, errors from parent parts should propagate to their
+    // children.
+    //
+    // [*] Not in the context of the package, anyway. Skyframe values containing a package may
+    // serialize events emitted during its construction/evaluation.
+    @Nullable private FailureDetail failureDetailOverride = null;
 
     private ImmutableList<Label> defaultApplicableLicenses = ImmutableList.of();
     private License defaultLicense = License.NO_LICENSE;
@@ -1145,9 +1196,10 @@
       return this;
     }
 
-    Builder setIOExceptionAndMessage(IOException e, String message) {
+    Builder setIOException(IOException e, String message, DetailedExitCode detailedExitCode) {
       this.ioException = e;
       this.ioExceptionMessage = message;
+      this.ioExceptionDetailedExitCode = detailedExitCode;
       return setContainsErrors();
     }
 
@@ -1182,6 +1234,28 @@
       return this;
     }
 
+    public void setFailureDetailOverride(FailureDetail failureDetail) {
+      failureDetailOverride = failureDetail;
+    }
+
+    @Nullable
+    public FailureDetail getFailureDetail() {
+      if (failureDetailOverride != null) {
+        return failureDetailOverride;
+      }
+
+      for (Event event : this.events) {
+        if (event.getKind() != EventKind.ERROR) {
+          continue;
+        }
+        DetailedExitCode detailedExitCode = event.getProperty(DetailedExitCode.class);
+        if (detailedExitCode != null && detailedExitCode.getFailureDetail() != null) {
+          return detailedExitCode.getFailureDetail();
+        }
+      }
+      return null;
+    }
+
     Builder setStarlarkFileDependencies(ImmutableList<Label> starlarkFileDependencies) {
       this.starlarkFileDependencies = starlarkFileDependencies;
       return this;
@@ -1231,8 +1305,8 @@
 
     /**
      * Sets the default value to use for a rule's {@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR}
-     * attribute when not explicitly specified by the rule. Records a package error if
-     * any labels are duplicated.
+     * attribute when not explicitly specified by the rule. Records a package error if any labels
+     * are duplicated.
      */
     void setDefaultCompatibleWith(List<Label> environments, String attrName, Location location) {
       if (hasDuplicateLabels(
@@ -1427,8 +1501,12 @@
         EventHandler eventHandler) {
       Set<Label> dupes = CollectionUtils.duplicatedElementsOf(labels);
       for (Label dupe : dupes) {
-        eventHandler.handle(Event.error(location, String.format(
-            "label '%s' is duplicated in the '%s' list of '%s'", dupe, attrName, owner)));
+        eventHandler.handle(
+            error(
+                location,
+                String.format(
+                    "label '%s' is duplicated in the '%s' list of '%s'", dupe, attrName, owner),
+                Code.DUPLICATE_LABEL));
       }
       return !dupes.isEmpty();
     }
@@ -1467,8 +1545,13 @@
       for (Label environment : group.getEnvironments()) {
         EnvironmentGroup otherGroup = environmentGroups.get(environment);
         if (otherGroup != null) {
-          eventHandler.handle(Event.error(location, "environment " + environment + " belongs to"
-              + " both " + group.getLabel() + " and " + otherGroup.getLabel()));
+          eventHandler.handle(
+              error(
+                  location,
+                  String.format(
+                      "environment %s belongs to both %s and %s",
+                      environment, group.getLabel(), otherGroup.getLabel()),
+                  Code.ENVIRONMENT_IN_MULTIPLE_GROUPS));
           setContainsErrors();
         } else {
           environmentGroups.put(environment, group);
@@ -1518,7 +1601,8 @@
       Preconditions.checkNotNull(buildFileLabel);
       Preconditions.checkNotNull(makeEnv);
       if (ioException != null) {
-        throw new NoSuchPackageException(getPackageIdentifier(), ioExceptionMessage, ioException);
+        throw new NoSuchPackageException(
+            getPackageIdentifier(), ioExceptionMessage, ioException, ioExceptionDetailedExitCode);
       }
 
       // We create the original BUILD InputFile when the package filename is set; however, the
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
index 8d86c01..02825ae 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageFactory.java
@@ -42,6 +42,7 @@
 import com.google.devtools.build.lib.profiler.SilentCloseable;
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.server.FailureDetails.PackageLoading;
+import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code;
 import com.google.devtools.build.lib.syntax.Argument;
 import com.google.devtools.build.lib.syntax.CallExpression;
 import com.google.devtools.build.lib.syntax.DefStatement;
@@ -991,7 +992,11 @@
 
     // Report scan/parse errors.
     if (!file.ok()) {
-      Event.replayEventsOn(pkgContext.eventHandler, file.errors());
+      Event.replayEventsOn(
+          pkgContext.eventHandler,
+          file.errors(),
+          DetailedExitCode.class,
+          syntaxError -> Package.createDetailedCode(syntaxError.toString(), Code.SYNTAX_ERROR));
       return false;
     }
 
@@ -1000,7 +1005,8 @@
     // after we've parsed the BUILD file and created the Package.
     String error = LabelValidator.validatePackageName(packageId.getPackageFragment().toString());
     if (error != null) {
-      pkgContext.eventHandler.handle(Event.error(file.getStartLocation(), error));
+      pkgContext.eventHandler.handle(
+          Package.error(file.getStartLocation(), error, Code.PACKAGE_NAME_INVALID));
       return false;
     }
 
@@ -1022,7 +1028,11 @@
     try {
       prog = Program.compileFile(file, module);
     } catch (SyntaxError.Exception ex) {
-      Event.replayEventsOn(pkgContext.eventHandler, ex.errors());
+      Event.replayEventsOn(
+          pkgContext.eventHandler,
+          ex.errors(),
+          DetailedExitCode.class,
+          syntaxError -> Package.createDetailedCode(syntaxError.toString(), Code.SYNTAX_ERROR));
       return false;
     }
 
@@ -1082,7 +1092,8 @@
       try {
         Starlark.execFileProgram(prog, module, thread);
       } catch (EvalException ex) {
-        pkgContext.eventHandler.handle(Event.error(null, ex.getMessageWithStack()));
+        pkgContext.eventHandler.handle(
+            Package.error(null, ex.getMessageWithStack(), Code.STARLARK_EVAL_ERROR));
         return false;
       }
 
@@ -1119,7 +1130,7 @@
     NodeVisitor checker =
         new NodeVisitor() {
           void error(Location loc, String message) {
-            eventHandler.handle(Event.error(loc, message));
+            eventHandler.handle(Package.error(loc, message, Code.SYNTAX_ERROR));
             success[0] = false;
           }
 
diff --git a/src/main/java/com/google/devtools/build/lib/packages/PackageGroup.java b/src/main/java/com/google/devtools/build/lib/packages/PackageGroup.java
index a30657c..ca1aa12 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/PackageGroup.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/PackageGroup.java
@@ -18,10 +18,10 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.cmdline.Label;
-import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventHandler;
 import com.google.devtools.build.lib.packages.License.DistributionType;
 import com.google.devtools.build.lib.packages.PackageSpecification.PackageGroupContents;
+import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code;
 import com.google.devtools.build.lib.syntax.Location;
 import java.util.Collection;
 import java.util.Collections;
@@ -53,7 +53,7 @@
     this.containingPackage = pkg;
     this.includes = ImmutableList.copyOf(includes);
 
-    // TODO(bazel-team): Consider refactoring so constructor takes a PackageGroupContents. 
+    // TODO(bazel-team): Consider refactoring so constructor takes a PackageGroupContents.
     ImmutableList.Builder<PackageSpecification> packagesBuilder = ImmutableList.builder();
     for (String packageSpecification : packageSpecifications) {
       PackageSpecification specification = null;
@@ -63,7 +63,8 @@
                 label.getPackageIdentifier().getRepository(), packageSpecification);
       } catch (PackageSpecification.InvalidPackageSpecificationException e) {
         containsErrors = true;
-        eventHandler.handle(Event.error(location, e.getMessage()));
+        eventHandler.handle(
+            Package.error(location, e.getMessage(), Code.INVALID_PACKAGE_SPECIFICATION));
       }
 
       if (specification != null) {
diff --git a/src/main/java/com/google/devtools/build/lib/packages/StarlarkLibrary.java b/src/main/java/com/google/devtools/build/lib/packages/StarlarkLibrary.java
index c7f993d..3fe9883 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/StarlarkLibrary.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/StarlarkLibrary.java
@@ -19,11 +19,11 @@
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.collect.nestedset.Depset;
-import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.packages.License.DistributionType;
 import com.google.devtools.build.lib.packages.PackageFactory.PackageContext;
 import com.google.devtools.build.lib.packages.Type.ConversionException;
 import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
+import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code;
 import com.google.devtools.build.lib.syntax.Dict;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Location;
@@ -296,7 +296,8 @@
         License license = BuildType.LICENSE.convert(licensesList, "'licenses' operand");
         context.pkgBuilder.setDefaultLicense(license);
       } catch (ConversionException e) {
-        context.eventHandler.handle(Event.error(thread.getCallerLocation(), e.getMessage()));
+        context.eventHandler.handle(
+            Package.error(thread.getCallerLocation(), e.getMessage(), Code.LICENSE_PARSE_FAILURE));
         context.pkgBuilder.setContainsErrors();
       }
       return Starlark.NONE;
@@ -320,7 +321,9 @@
             BuildType.DISTRIBUTIONS.convert(object, "'distribs' operand");
         context.pkgBuilder.setDefaultDistribs(distribs);
       } catch (ConversionException e) {
-        context.eventHandler.handle(Event.error(thread.getCallerLocation(), e.getMessage()));
+        context.eventHandler.handle(
+            Package.error(
+                thread.getCallerLocation(), e.getMessage(), Code.DISTRIBUTIONS_PARSE_FAILURE));
         context.pkgBuilder.setContainsErrors();
       }
       return Starlark.NONE;
diff --git a/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java b/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java
index 975a592..34caa41 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/StarlarkNativeModule.java
@@ -30,6 +30,7 @@
 import com.google.devtools.build.lib.packages.RuleClass.Builder.ThirdPartyLicenseExistencePolicy;
 import com.google.devtools.build.lib.packages.Type.ConversionException;
 import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
+import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code;
 import com.google.devtools.build.lib.starlarkbuildapi.StarlarkNativeModuleApi;
 import com.google.devtools.build.lib.syntax.Dict;
 import com.google.devtools.build.lib.syntax.EvalException;
@@ -42,6 +43,7 @@
 import com.google.devtools.build.lib.syntax.StarlarkThread;
 import com.google.devtools.build.lib.syntax.StarlarkValue;
 import com.google.devtools.build.lib.syntax.Tuple;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -110,8 +112,9 @@
               excludes.isEmpty() ? "" : " - [" + Joiner.on(", ").join(excludes) + "]",
               e.getMessage());
       Location loc = thread.getCallerLocation();
-      context.eventHandler.handle(Event.error(loc, errorMessage));
-      context.pkgBuilder.setIOExceptionAndMessage(e, errorMessage);
+      Event error = Package.error(loc, errorMessage, Code.GLOB_IO_EXCEPTION);
+      context.eventHandler.handle(error);
+      context.pkgBuilder.setIOException(e, errorMessage, error.getProperty(DetailedExitCode.class));
       matches = ImmutableList.of();
     } catch (BadGlobException e) {
       throw new EvalException(e);
diff --git a/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactory.java b/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactory.java
index d0eec4d..dadcacd 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactory.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.events.StoredEventHandler;
 import com.google.devtools.build.lib.packages.Package.NameConflictException;
 import com.google.devtools.build.lib.packages.PackageFactory.EnvironmentExtension;
+import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code;
 import com.google.devtools.build.lib.syntax.Dict;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Module;
@@ -192,7 +193,8 @@
         try {
           Starlark.execFileProgram(prog, module, thread);
         } catch (EvalException ex) {
-          localReporter.handle(Event.error(null, ex.getMessageWithStack()));
+          localReporter.handle(
+              Package.error(null, ex.getMessageWithStack(), Code.STARLARK_EVAL_ERROR));
         }
       }
 
@@ -232,6 +234,9 @@
     if (aPackage.containsErrors()) {
       builder.setContainsErrors();
     }
+    if (aPackage.getFailureDetail() != null) {
+      builder.setFailureDetailOverride(aPackage.getFailureDetail());
+    }
     builder.addRegisteredExecutionPlatforms(aPackage.getRegisteredExecutionPlatforms());
     builder.addRegisteredToolchains(aPackage.getRegisteredToolchains());
     builder.addRepositoryMappings(aPackage);
diff --git a/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEnvironment.java
index da42901..073bd11 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/engine/QueryEnvironment.java
@@ -477,7 +477,6 @@
    * QueryException} or emitting an event, depending on whether the evaluation is running in a "keep
    * going" mode.
    */
-  // TODO(b/138456686): make detailedExitCode non-nullable
   void handleError(
       QueryExpression expression, String message, @Nullable DetailedExitCode detailedExitCode)
       throws QueryException;
diff --git a/src/main/java/com/google/devtools/build/lib/query2/query/BlazeQueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/query/BlazeQueryEnvironment.java
index 181ff3c..3824acc 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/query/BlazeQueryEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/query/BlazeQueryEnvironment.java
@@ -323,7 +323,7 @@
       handleError(
           caller,
           "errors were encountered while computing transitive closure",
-          /*detailedExitCode=*/ null);
+          errorObserver.getDetailedExitCode());
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/query2/query/ErrorPrintingTargetEdgeErrorObserver.java b/src/main/java/com/google/devtools/build/lib/query2/query/ErrorPrintingTargetEdgeErrorObserver.java
index eab4435..370d5e4 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/query/ErrorPrintingTargetEdgeErrorObserver.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/query/ErrorPrintingTargetEdgeErrorObserver.java
@@ -21,10 +21,11 @@
 import com.google.devtools.build.lib.packages.NoSuchThingException;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 
 /**
  * Record errors, such as missing package/target or rules containing errors, encountered during
- * visitation. Emit an error message upon encountering missing edges
+ * visitation. Emit an error message upon encountering missing edges.
  *
  * <p>The accessor {@link #hasErrors()}) may not be called until the concurrent phase is over, i.e.
  * all external calls to visit() methods have completed.
@@ -43,9 +44,12 @@
   @ThreadSafety.ThreadSafe
   @Override
   public void missingEdge(Target target, Label label, NoSuchThingException e) {
+    DetailedExitCode detailedExitCode = e.getDetailedExitCode();
     eventHandler.handle(
         Event.error(
-            TargetUtils.getLocationMaybe(target), TargetUtils.formatMissingEdge(target, label, e)));
+                TargetUtils.getLocationMaybe(target),
+                TargetUtils.formatMissingEdge(target, label, e))
+            .withProperty(DetailedExitCode.class, detailedExitCode));
     super.missingEdge(target, label, e);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/query2/query/GraphlessBlazeQueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/query/GraphlessBlazeQueryEnvironment.java
index bf2f887..4406229 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/query/GraphlessBlazeQueryEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/query/GraphlessBlazeQueryEnvironment.java
@@ -230,7 +230,7 @@
       handleError(
           caller,
           "errors were encountered while computing transitive closure",
-          /*detailedExitCode=*/ null);
+          errorObserver.getDetailedExitCode());
     }
     callback.process(result);
   }
@@ -306,7 +306,7 @@
       handleError(
           caller,
           "errors were encountered while computing transitive closure",
-          /*detailedExitCode=*/ null);
+          errorObserver.getDetailedExitCode());
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/query2/query/TargetEdgeErrorObserver.java b/src/main/java/com/google/devtools/build/lib/query2/query/TargetEdgeErrorObserver.java
index 725ffcc..cadea50 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/query/TargetEdgeErrorObserver.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/query/TargetEdgeErrorObserver.java
@@ -21,6 +21,10 @@
 import com.google.devtools.build.lib.packages.Rule;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.pkgcache.TargetEdgeObserver;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.util.DetailedExitCode;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.Nullable;
 
 /**
  * Record errors, such as missing package/target or rules containing errors, encountered during
@@ -41,6 +45,8 @@
    */
   private volatile boolean hasErrors = false;
 
+  private final AtomicReference<DetailedExitCode> errorCode = new AtomicReference<>();
+
   /**
    * Reports an unresolved label error and records the fact that an error was encountered.
    *
@@ -52,6 +58,7 @@
   @Override
   public void missingEdge(Target target, Label label, NoSuchThingException e) {
     hasErrors = true;
+    errorCode.compareAndSet(/*expect=*/ null, /*update=*/ e.getDetailedExitCode());
   }
 
   /**
@@ -67,6 +74,12 @@
     return hasErrors;
   }
 
+  /** Returns the first {@link DetailedExitCode} encountered, or {@code null} if there were none. */
+  @Nullable
+  public DetailedExitCode getDetailedExitCode() {
+    return errorCode.get();
+  }
+
   @Override
   public void edge(Target from, Attribute attribute, Target to) {
     // No-op.
@@ -76,7 +89,11 @@
   public void node(Target node) {
     if (node.getPackage().containsErrors()
         || ((node instanceof Rule) && ((Rule) node).containsErrors())) {
-      this.hasErrors = true; // Note, this is thread-safe.
+      this.hasErrors = true;
+      FailureDetail failureDetail = node.getPackage().getFailureDetail();
+      if (failureDetail != null) {
+        errorCode.compareAndSet(/*expect=*/ null, /*update=*/ DetailedExitCode.of(failureDetail));
+      }
     }
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
index 7273a1e..f92fb2d 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
@@ -58,6 +58,7 @@
 import com.google.devtools.build.lib.rules.repository.WorkspaceFileHelper;
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.server.FailureDetails.PackageLoading;
+import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code;
 import com.google.devtools.build.lib.skyframe.BzlLoadFunction.BzlLoadFailedException;
 import com.google.devtools.build.lib.skyframe.GlobValue.InvalidGlobPatternException;
 import com.google.devtools.build.lib.syntax.EvalException;
@@ -854,7 +855,7 @@
     String message =
         ContainingPackageLookupValue.getErrorMessageForLabelCrossingPackageBoundary(
             pkgRoot, label, containingPkgLookupValue);
-    pkgBuilder.addEvent(Event.error(location, message));
+    pkgBuilder.addEvent(Package.error(location, message, Code.LABEL_CROSSES_PACKAGE_BOUNDARY));
     return true;
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java
index 4de40d1..50a0330 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java
@@ -30,6 +30,7 @@
 import com.google.devtools.build.lib.pkgcache.TargetPatternPreloader;
 import com.google.devtools.build.lib.server.FailureDetails.TargetPatterns;
 import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternKey;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.ErrorInfo;
 import com.google.devtools.build.skyframe.EvaluationResult;
@@ -89,8 +90,7 @@
           if (!keepGoing) {
             throw e;
           }
-          eventHandler.handle(
-              Event.error("Skipping '" + patternLookup.pattern + "': " + e.getMessage()));
+          eventHandler.handle(createPatternParsingError(e, patternLookup.pattern));
           eventHandler.post(PatternExpandingError.skipped(patternLookup.pattern, e.getMessage()));
           resultBuilder.put(patternLookup.pattern, ImmutableSet.of());
         }
@@ -126,7 +126,7 @@
           throw new IllegalStateException(error.toString());
         }
         if (keepGoing) {
-          eventHandler.handle(Event.error("Skipping '" + rawPattern + "': " + errorMessage));
+          eventHandler.handle(createPatternParsingError(targetParsingException, rawPattern));
           eventHandler.post(PatternExpandingError.skipped(rawPattern, errorMessage));
         } else {
           eventHandler.post(PatternExpandingError.failed(patternLookup.pattern, errorMessage));
@@ -158,7 +158,7 @@
       if (!keepGoing) {
         throw e;
       }
-      eventHandler.handle(Event.error("Skipping '" + targetPattern + "': " + e.getMessage()));
+      eventHandler.handle(createPatternParsingError(e, targetPattern));
       return null;
     }
   }
@@ -179,6 +179,11 @@
     throw new AssertionError();
   }
 
+  private static Event createPatternParsingError(TargetParsingException e, String pattern) {
+    return Event.error("Skipping '" + pattern + "': " + e.getMessage())
+        .withProperty(DetailedExitCode.class, e.getDetailedExitCode());
+  }
+
   private abstract static class PatternLookup {
     protected final String pattern;
     @Nullable private final SkyKey skyKey;
diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto
index 4b27892..533df4b 100644
--- a/src/main/protobuf/failure_details.proto
+++ b/src/main/protobuf/failure_details.proto
@@ -1115,6 +1115,21 @@
     PACKAGE_MISSING = 11 [(metadata) = { exit_code: 1 }];
     TARGET_MISSING = 12 [(metadata) = { exit_code: 1 }];
     NO_SUCH_THING = 13 [(metadata) = { exit_code: 1 }];
+    GLOB_IO_EXCEPTION = 14 [(metadata) = { exit_code: 1 }];
+    DUPLICATE_LABEL = 15 [(metadata) = { exit_code: 1 }];
+    INVALID_PACKAGE_SPECIFICATION = 16 [(metadata) = { exit_code: 1 }];
+    SYNTAX_ERROR = 17 [(metadata) = { exit_code: 1 }];
+    ENVIRONMENT_IN_DIFFERENT_PACKAGE = 18 [(metadata) = { exit_code: 1 }];
+    DEFAULT_ENVIRONMENT_UNDECLARED = 19 [(metadata) = { exit_code: 1 }];
+    ENVIRONMENT_IN_MULTIPLE_GROUPS = 20 [(metadata) = { exit_code: 1 }];
+    ENVIRONMENT_DOES_NOT_EXIST = 21 [(metadata) = { exit_code: 1 }];
+    ENVIRONMENT_INVALID = 22 [(metadata) = { exit_code: 1 }];
+    ENVIRONMENT_NOT_IN_GROUP = 23 [(metadata) = { exit_code: 1 }];
+    PACKAGE_NAME_INVALID = 24 [(metadata) = { exit_code: 1 }];
+    STARLARK_EVAL_ERROR = 25 [(metadata) = { exit_code: 1 }];
+    LICENSE_PARSE_FAILURE = 26 [(metadata) = { exit_code: 1 }];
+    DISTRIBUTIONS_PARSE_FAILURE = 27 [(metadata) = { exit_code: 1 }];
+    LABEL_CROSSES_PACKAGE_BOUNDARY = 28 [(metadata) = { exit_code: 1 }];
   }
 
   Code code = 1;
diff --git a/src/test/java/com/google/devtools/build/lib/query2/cquery/BUILD b/src/test/java/com/google/devtools/build/lib/query2/cquery/BUILD
index 558e4b0..48cecfd 100644
--- a/src/test/java/com/google/devtools/build/lib/query2/cquery/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/query2/cquery/BUILD
@@ -54,6 +54,7 @@
         "//src/main/java/com/google/devtools/build/lib/query2/engine",
         "//src/main/java/com/google/devtools/build/lib/util:filetype",
         "//src/main/java/com/google/devtools/build/lib/vfs",
+        "//src/main/protobuf:failure_details_java_proto",
         "//src/test/java/com/google/devtools/build/lib/analysis/util",
         "//src/test/java/com/google/devtools/build/lib/query2/testutil",
         "//third_party:guava",
diff --git a/src/test/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetQuerySemanticsTest.java b/src/test/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetQuerySemanticsTest.java
index e091461..688550c 100644
--- a/src/test/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetQuerySemanticsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/query2/cquery/ConfiguredTargetQuerySemanticsTest.java
@@ -39,6 +39,7 @@
 import com.google.devtools.build.lib.packages.BuildType;
 import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting;
 import com.google.devtools.build.lib.query2.engine.QueryException;
+import com.google.devtools.build.lib.server.FailureDetails.ConfigurableQuery.Code;
 import com.google.devtools.build.lib.util.FileTypeSet;
 import com.google.devtools.build.lib.vfs.Path;
 import java.util.Collection;
@@ -163,7 +164,9 @@
     String targetConfiguration = myRule.getConfigurationChecksum();
 
     // Test that the proper error is thrown when requesting an attribute that doesn't exist.
-    assertThat(evalThrows("labels('fake_attr', //test:my_rule)", true))
+    EvalThrowsResult evalThrowsResult = evalThrows("labels('fake_attr', //test:my_rule)", true);
+    assertConfigurableQueryCode(evalThrowsResult.getFailureDetail(), Code.ATTRIBUTE_MISSING);
+    assertThat(evalThrowsResult.getMessage())
         .isEqualTo(
             String.format(
                 "in 'fake_attr' of rule //test:my_rule:  ConfiguredTarget(//test:my_rule, %s) "
@@ -336,10 +339,14 @@
     getHelper().setWholeTestUniverseScope("test:my_rule");
 
     assertThat(eval("config(//test:target_dep, target)")).isEqualTo(eval("//test:target_dep"));
-    assertThat(evalThrows("config(//test:host_dep, target)", true))
+    EvalThrowsResult hostResult = evalThrows("config(//test:host_dep, target)", true);
+    assertThat(hostResult.getMessage())
         .isEqualTo("No target (in) //test:host_dep could be found in the 'target' configuration");
-    assertThat(evalThrows("config(//test:exec_dep, target)", true))
+    assertConfigurableQueryCode(hostResult.getFailureDetail(), Code.TARGET_MISSING);
+    EvalThrowsResult execResult = evalThrows("config(//test:exec_dep, target)", true);
+    assertThat(execResult.getMessage())
         .isEqualTo("No target (in) //test:exec_dep could be found in the 'target' configuration");
+    assertConfigurableQueryCode(execResult.getFailureDetail(), Code.TARGET_MISSING);
 
     BuildConfiguration configuration =
         getConfiguration(Iterables.getOnlyElement(eval("config(//test:dep, target)")));
@@ -356,11 +363,15 @@
 
     getHelper().setWholeTestUniverseScope("test:my_rule");
 
-    assertThat(evalThrows("config(//test:target_dep, host)", true))
+    EvalThrowsResult targetResult = evalThrows("config(//test:target_dep, host)", true);
+    assertThat(targetResult.getMessage())
         .isEqualTo("No target (in) //test:target_dep could be found in the 'host' configuration");
+    assertConfigurableQueryCode(targetResult.getFailureDetail(), Code.TARGET_MISSING);
     assertThat(eval("config(//test:host_dep, host)")).isEqualTo(eval("//test:host_dep"));
-    assertThat(evalThrows("config(//test:exec_dep, host)", true))
+    EvalThrowsResult hostResult = evalThrows("config(//test:exec_dep, host)", true);
+    assertThat(hostResult.getMessage())
         .isEqualTo("No target (in) //test:exec_dep could be found in the 'host' configuration");
+    assertConfigurableQueryCode(hostResult.getFailureDetail(), Code.TARGET_MISSING);
 
     BuildConfiguration configuration =
         getConfiguration(Iterables.getOnlyElement(eval("config(//test:dep, host)")));
@@ -448,6 +459,7 @@
     QueryException e =
         assertThrows(
             QueryException.class, () -> eval("config(//mytest:mytarget," + wrongPrefix + ")"));
+    assertConfigurableQueryCode(e.getFailureDetail(), Code.INCORRECT_CONFIG_ARGUMENT_ERROR);
     assertThat(e)
         .hasMessageThat()
         .contains("config()'s second argument must identify a unique configuration");
@@ -478,7 +490,7 @@
 
     helper.setKeepGoing(false);
     getHelper().turnOffFailFast();
-    assertThat(evalThrows("//parent/...", true))
+    assertThat(evalThrows("//parent/...", true).getMessage())
         .isEqualTo(
             "no such package 'parent/child': Symlink cycle detected while trying to "
                 + "find BUILD file /workspace/parent/child/BUILD");
diff --git a/src/test/java/com/google/devtools/build/lib/query2/testutil/AbstractQueryTest.java b/src/test/java/com/google/devtools/build/lib/query2/testutil/AbstractQueryTest.java
index 6e80330..591b604 100644
--- a/src/test/java/com/google/devtools/build/lib/query2/testutil/AbstractQueryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/query2/testutil/AbstractQueryTest.java
@@ -35,6 +35,7 @@
 import com.google.devtools.build.lib.graph.LabelSerializer;
 import com.google.devtools.build.lib.graph.Node;
 import com.google.devtools.build.lib.packages.BuildType;
+import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.packages.util.MockToolsConfig;
 import com.google.devtools.build.lib.query2.engine.DigraphQueryEvalResult;
 import com.google.devtools.build.lib.query2.engine.QueryEnvironment;
@@ -43,6 +44,10 @@
 import com.google.devtools.build.lib.query2.engine.QueryEvalResult;
 import com.google.devtools.build.lib.query2.engine.QueryException;
 import com.google.devtools.build.lib.query2.testutil.AbstractQueryTest.QueryHelper.ResultAndTargets;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code;
+import com.google.devtools.build.lib.server.FailureDetails.Query;
+import com.google.devtools.build.lib.server.FailureDetails.TargetPatterns;
 import com.google.devtools.build.lib.testutil.TestConstants;
 import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
 import com.google.devtools.build.lib.vfs.Path;
@@ -167,15 +172,39 @@
     return result.getResultSet();
   }
 
-  // Like eval(), but asserts that evaluation completes abruptly with a
-  // QueryException, whose message is returned.
-  protected String evalThrows(String query, boolean unconditionallyThrows) throws Exception {
+  // Like eval(), but asserts that evaluation completes abruptly with a QueryException, whose
+  // message and FailureDetail is returned.
+  protected EvalThrowsResult evalThrows(String query, boolean unconditionallyThrows)
+      throws Exception {
     try {
       helper.evaluateQuery(query);
       fail("evaluateQuery completed normally: " + query);
-      return null; // unreachable
+      throw new IllegalStateException();
     } catch (QueryException e) {
-      return e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
+      String message = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
+      return new EvalThrowsResult(message, e.getFailureDetail());
+    }
+  }
+
+  /**
+   * Error message and {@link FailureDetail} from the failing query evaluation performed by {@link
+   * #evalThrows}.
+   */
+  protected static class EvalThrowsResult {
+    private final String message;
+    private final FailureDetail failureDetail;
+
+    protected EvalThrowsResult(String message, FailureDetail failureDetail) {
+      this.message = message;
+      this.failureDetail = failureDetail;
+    }
+
+    public String getMessage() {
+      return message;
+    }
+
+    public FailureDetail getFailureDetail() {
+      return failureDetail;
     }
   }
 
@@ -206,10 +235,32 @@
     assertThat(x.containsAll(y)).isFalse();
   }
 
+  protected static void assertPackageLoadingCode(ResultAndTargets<Target> result, Code code) {
+    FailureDetail failureDetail =
+        result.getQueryEvalResult().getDetailedExitCode().getFailureDetail();
+    assertThat(failureDetail).isNotNull();
+    assertPackageLoadingCode(failureDetail, code);
+  }
+
+  protected static void assertPackageLoadingCode(FailureDetail failureDetail, Code code) {
+    assertThat(failureDetail.getPackageLoading().getCode()).isEqualTo(code);
+  }
+
+  protected static void assertQueryCode(ResultAndTargets<Target> result, Query.Code code) {
+    FailureDetail failureDetail =
+        result.getQueryEvalResult().getDetailedExitCode().getFailureDetail();
+    assertThat(failureDetail).isNotNull();
+    assertQueryCode(failureDetail, code);
+  }
+
+  protected static void assertQueryCode(FailureDetail failureDetail, Query.Code code) {
+    assertThat(failureDetail.getQuery().getCode()).isEqualTo(code);
+  }
+
   @Test
   public void testTargetLiteralWithMissingTargets() throws Exception {
     writeFile("a/BUILD");
-    assertThat(evalThrows("//a:b", false))
+    assertThat(evalThrows("//a:b", false).getMessage())
         .isEqualTo(
             "no such target '//a:b': target 'b' not declared in package 'a' "
                 + "defined by "
@@ -239,8 +290,16 @@
 
   @Test
   public void testBadTargetLiterals() throws Exception {
-    assertThat(evalThrows("bad:*:*", false))
-        .isEqualTo("Invalid package name 'bad:*': " + BAD_PACKAGE_NAME);
+    runBadTargetLiteralsTest(true);
+  }
+
+  protected void runBadTargetLiteralsTest(boolean checkDetailedCode) throws Exception {
+    EvalThrowsResult result = evalThrows("bad:*:*", false);
+    if (checkDetailedCode) {
+      assertThat(result.getFailureDetail().getTargetPatterns().getCode())
+          .isEqualTo(TargetPatterns.Code.LABEL_SYNTAX_ERROR);
+    }
+    assertThat(result.getMessage()).isEqualTo("Invalid package name 'bad:*': " + BAD_PACKAGE_NAME);
   }
 
   @Test
@@ -391,7 +450,9 @@
     assertContains(eval("c:*"), eval("some(c:*)"));
     assertThat(evalToString("some(//c:q)")).isEqualTo("//c:q");
 
-    assertThat(evalThrows("some(//c:q intersect //c:p)", true)).isEqualTo("argument set is empty");
+    EvalThrowsResult result = evalThrows("some(//c:q intersect //c:p)", true);
+    assertThat(result.getMessage()).isEqualTo("argument set is empty");
+    assertQueryCode(result.getFailureDetail(), Query.Code.ARGUMENTS_MISSING);
   }
 
   protected void writeBuildFiles3() throws Exception {
@@ -784,7 +845,9 @@
     assertContains(
         eval("//b + //c + //d"),
         eval("let x = //a in deps($x) except $x" + getDependencyCorrectionWithGen()));
-    assertThat(evalThrows("$undefined", true)).isEqualTo("undefined variable 'undefined'");
+    EvalThrowsResult result = evalThrows("$undefined", true);
+    assertThat(result.getMessage()).isEqualTo("undefined variable 'undefined'");
+    assertQueryCode(result.getFailureDetail(), Query.Code.VARIABLE_UNDEFINED);
   }
 
   @Test
@@ -944,13 +1007,20 @@
 
   @Test
   public void testCycleInStarlark() throws Exception {
+    runCycleInStarlarkTest(/*checkFailureDetail=*/ true);
+  }
+
+  protected void runCycleInStarlarkTest(boolean checkFailureDetail) throws Exception {
     writeFile("a/BUILD", "load('//a:cycle1.bzl', 'C1')", "sh_library(name = 'a')");
     writeFile("a/cycle1.bzl", "load('//a:cycle2.bzl', 'C2')", "C1 = struct()");
     writeFile("a/cycle2.bzl", "load('//a:cycle1.bzl', 'C1')", "C2 = struct()");
-    try {
-      evalThrows("//a:all", false);
-    } catch (QueryException e) {
-      // Expected.
+    EvalThrowsResult result = evalThrows("//a:all", false);
+    // TODO(mschaller): evalThrows's message can be non-deterministic if events are too. It probably
+    //  needs to be refactored to deal with underlying event non-determinism, because fixing query
+    //  engines' event non-determinism is probably hard.
+    if (checkFailureDetail) {
+      assertThat(result.getFailureDetail().getTargetPatterns().getCode())
+          .isEqualTo(TargetPatterns.Code.CYCLE);
     }
   }
 
@@ -1075,9 +1145,11 @@
     writeFile("c/BUILD", "test_suite(name='c', tests=['//d'])");
     writeFile("d/BUILD");
 
+    EvalThrowsResult result = evalThrows("tests(//c)", false);
     assertStartsWith(
         "couldn't expand 'tests' attribute of test_suite //c:c: " + "no such target '//d:d'",
-        evalThrows("tests(//c)", false));
+        result.getMessage());
+    assertPackageLoadingCode(result.getFailureDetail(), Code.TARGET_MISSING);
   }
 
   @Test
@@ -1199,7 +1271,7 @@
     writeFile("x/BUILD", "cc_library(name='x', srcs=['a.cc', 'a.cc'])");
     String expectedError = "Label '//x:a.cc' is duplicated in the 'srcs' attribute of rule 'x'";
     if (helper.isKeepGoing()) {
-      assertThat(evalThrows("//x", false)).isEqualTo(expectedError);
+      assertThat(evalThrows("//x", false).getMessage()).isEqualTo(expectedError);
     } else {
       evalThrows("//x", false);
       assertContainsEvent(expectedError);
@@ -1353,10 +1425,12 @@
   public void testStrictTestSuiteWithFile() throws Exception {
     helper.setQuerySettings(Setting.TESTS_EXPRESSION_STRICT);
     writeFile("x/BUILD", "test_suite(name='a', tests=['a.txt'])");
-    assertThat(evalThrows("tests(//x:a)", false))
+    EvalThrowsResult result = evalThrows("tests(//x:a)", false);
+    assertThat(result.getMessage())
         .isEqualTo(
             "The label '//x:a.txt' in the test_suite '//x:a' does not refer to a test or "
                 + "test_suite rule!");
+    assertQueryCode(result.getFailureDetail(), Query.Code.INVALID_LABEL_IN_TEST_SUITE);
   }
 
   @Test
@@ -1430,10 +1504,13 @@
     // When the query environment is queried for targets belonging to packages beneath the
     // package "a/b", which doesn't exist,
     String missingPackage = "a/b";
-    String s = evalThrows("//" + missingPackage + "/...", false);
+    EvalThrowsResult result = evalThrows("//" + missingPackage + "/...", false);
+    String s = result.getMessage();
 
     // Then an exception is thrown that says that the pattern matched nothing.
     assertThat(s).containsMatch("no targets found beneath '" + missingPackage + "'");
+    assertThat(result.getFailureDetail().getTargetPatterns().getCode())
+        .isEqualTo(TargetPatterns.Code.TARGETS_MISSING);
   }
 
   @Test
@@ -2050,3 +2127,4 @@
     String getLabel(T target);
   }
 }
+
diff --git a/src/test/java/com/google/devtools/build/lib/query2/testutil/PostAnalysisQueryTest.java b/src/test/java/com/google/devtools/build/lib/query2/testutil/PostAnalysisQueryTest.java
index 3aec7e5..93bcc69 100644
--- a/src/test/java/com/google/devtools/build/lib/query2/testutil/PostAnalysisQueryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/query2/testutil/PostAnalysisQueryTest.java
@@ -38,6 +38,8 @@
 import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting;
 import com.google.devtools.build.lib.query2.engine.QueryExpression;
 import com.google.devtools.build.lib.query2.engine.QueryParser;
+import com.google.devtools.build.lib.server.FailureDetails.ConfigurableQuery.Code;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
 import com.google.devtools.build.lib.util.FileTypeSet;
 import java.util.Collections;
@@ -101,9 +103,10 @@
   }
 
   @Override
-  protected String evalThrows(String query, boolean unconditionallyThrows) throws Exception {
+  protected EvalThrowsResult evalThrows(String query, boolean unconditionallyThrows)
+      throws Exception {
     maybeParseUniverseScope(query);
-    String queryResult = super.evalThrows(query, unconditionallyThrows);
+    EvalThrowsResult queryResult = super.evalThrows(query, unconditionallyThrows);
     if (!getHelper().isWholeTestUniverse()) {
       helper.setUniverseScope(getDefaultUniverseScope());
     }
@@ -165,7 +168,8 @@
   @Test
   public void testBadTargetLiterals() throws Exception {
     getHelper().turnOffFailFast();
-    super.testBadTargetLiterals();
+    // Post-analysis query test infrastructure clobbers certain detailed failures.
+    runBadTargetLiteralsTest(/*checkDetailedCode=*/ false);
   }
 
   @Override
@@ -436,22 +440,26 @@
   @Test
   public void testVisibleFunctionDoesNotWork() throws Exception {
     writeSimpleTarget();
-    assertThat(evalThrows("visible(//test:target, //test:*)", true))
-        .isEqualTo("visible() is not supported on configured targets");
+    EvalThrowsResult result = evalThrows("visible(//test:target, //test:*)", true);
+    assertThat(result.getMessage()).isEqualTo("visible() is not supported on configured targets");
+    assertConfigurableQueryCode(result.getFailureDetail(), Code.VISIBLE_FUNCTION_NOT_SUPPORTED);
   }
 
   @Test
   public void testSiblingsFunctionDoesNotWork() throws Exception {
     writeSimpleTarget();
-    assertThat(evalThrows("siblings(//test:target)", true))
-        .isEqualTo("siblings() not supported for post analysis queries");
+    EvalThrowsResult result = evalThrows("siblings(//test:target)", true);
+    assertThat(result.getMessage()).isEqualTo("siblings() not supported for post analysis queries");
+    assertConfigurableQueryCode(result.getFailureDetail(), Code.SIBLINGS_FUNCTION_NOT_SUPPORTED);
   }
 
   @Test
   public void testBuildfilesFunctionDoesNotWork() throws Exception {
     writeSimpleTarget();
-    assertThat(evalThrows("buildfiles(//test:target)", true))
+    EvalThrowsResult result = evalThrows("buildfiles(//test:target)", true);
+    assertThat(result.getMessage())
         .isEqualTo("buildfiles() doesn't make sense for the configured target graph");
+    assertConfigurableQueryCode(result.getFailureDetail(), Code.BUILDFILES_FUNCTION_NOT_SUPPORTED);
   }
 
   // LabelListAttr not currently supported.
@@ -685,4 +693,8 @@
   @Override
   @Test
   public void testDefaultVisibilityReturnedInDeps_nonEmptyDependencyFilter() throws Exception {}
+
+  protected static void assertConfigurableQueryCode(FailureDetail failureDetail, Code code) {
+    assertThat(failureDetail.getConfigurableQuery().getCode()).isEqualTo(code);
+  }
 }