Encode remaining ActionExecution failures with FailureDetails

This removes the last uses of undetailed ActionExecutionException
constructors by adding details to {Test,User}ExecException.

RELNOTES: None.
PiperOrigin-RevId: 318143448
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
index 87cb251..73e9d68 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionException.java
@@ -51,15 +51,6 @@
   }
 
   public ActionExecutionException(
-      String message, Throwable cause, ActionAnalysisMetadata action, boolean catastrophe) {
-    super(message, cause);
-    this.action = action;
-    this.catastrophe = catastrophe;
-    this.detailedExitCode = DetailedExitCode.justExitCode(ExitCode.BUILD_FAILURE);
-    this.rootCauses = rootCausesFromAction(action, detailedExitCode);
-  }
-
-  public ActionExecutionException(
       String message,
       Throwable cause,
       ActionAnalysisMetadata action,
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Spawns.java b/src/main/java/com/google/devtools/build/lib/actions/Spawns.java
index dea5930..d84b8fc 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/Spawns.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/Spawns.java
@@ -14,6 +14,9 @@
 
 package com.google.devtools.build.lib.actions;
 
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.Spawn.Code;
 import com.google.devtools.build.lib.util.CommandDescriptionForm;
 import com.google.devtools.build.lib.util.CommandFailureUtils;
 import com.google.devtools.build.lib.vfs.Path;
@@ -94,22 +97,17 @@
     return customValue != null ? customValue : spawn.getMnemonic();
   }
 
-  /** Parse the timeout key in the spawn execution info, if it exists. Otherwise, return -1. */
+  /**
+   * Parse the timeout key in the spawn execution info, if it exists. Otherwise, return {@link
+   * Duration#ZERO}.
+   */
   public static Duration getTimeout(Spawn spawn) throws ExecException {
-    String timeoutStr = spawn.getExecutionInfo().get(ExecutionRequirements.TIMEOUT);
-    if (timeoutStr == null) {
-      return Duration.ZERO;
-    }
-    try {
-      return Duration.ofSeconds(Integer.parseInt(timeoutStr));
-    } catch (NumberFormatException e) {
-      throw new UserExecException("could not parse timeout: ", e);
-    }
+    return getTimeout(spawn, Duration.ZERO);
   }
 
   /**
    * Parse the timeout key in the spawn execution info, if it exists. Otherwise, return
-   * defaultTimeout, or 0 if that is null.
+   * defaultTimeout, or {@code Duration.ZERO} if that is null.
    */
   public static Duration getTimeout(Spawn spawn, Duration defaultTimeout) throws ExecException {
     String timeoutStr = spawn.getExecutionInfo().get(ExecutionRequirements.TIMEOUT);
@@ -119,7 +117,12 @@
     try {
       return Duration.ofSeconds(Integer.parseInt(timeoutStr));
     } catch (NumberFormatException e) {
-      throw new UserExecException("could not parse timeout: ", e);
+      throw new UserExecException(
+          e,
+          FailureDetail.newBuilder()
+              .setMessage("could not parse timeout")
+              .setSpawn(FailureDetails.Spawn.newBuilder().setCode(Code.INVALID_TIMEOUT))
+              .build());
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/actions/TestExecException.java b/src/main/java/com/google/devtools/build/lib/actions/TestExecException.java
index db2cb5d..8381507 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/TestExecException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/TestExecException.java
@@ -13,20 +13,30 @@
 // limitations under the License.
 package com.google.devtools.build.lib.actions;
 
-/**
- * An TestExecException that is related to the failure of a TestAction.
- */
-public final class TestExecException extends ExecException {
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.TestAction;
+import com.google.devtools.build.lib.util.DetailedExitCode;
 
-  public TestExecException(String message) {
+/** An TestExecException that is related to the failure of a TestAction. */
+public final class TestExecException extends ExecException {
+  private final FailureDetails.TestAction.Code detailedCode;
+
+  public TestExecException(String message, FailureDetails.TestAction.Code detailedCode) {
     super(message);
+    this.detailedCode = detailedCode;
   }
 
   @Override
-  public ActionExecutionException toActionExecutionException(String messagePrefix,
-      boolean verboseFailures, Action action) {
-    String message = messagePrefix + " failed";
-    return new ActionExecutionException(
-        message + ": " + getMessage(), this, action, isCatastrophic());
+  public ActionExecutionException toActionExecutionException(
+      String messagePrefix, boolean verboseFailures, Action action) {
+    String message = String.format("%s: %s", messagePrefix + " failed", getMessage());
+    DetailedExitCode code =
+        DetailedExitCode.of(
+            FailureDetail.newBuilder()
+                .setMessage(message)
+                .setTestAction(TestAction.newBuilder().setCode(detailedCode))
+                .build());
+    return new ActionExecutionException(message, this, action, isCatastrophic(), code);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java b/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java
index f58f9e1..caaeb66 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/UserExecException.java
@@ -14,29 +14,33 @@
 
 package com.google.devtools.build.lib.actions;
 
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.util.DetailedExitCode;
+
 /**
- * An ExecException that is related to the failure of an Action and therefore
- * very likely the user's fault.
+ * An ExecException that is related to the failure of an Action and therefore very likely the user's
+ * fault.
  */
 public class UserExecException extends ExecException {
 
-  public UserExecException(String message) {
-    super(message);
-  }
-  
-  public UserExecException(Throwable cause) {
-    super(cause);
+  private final FailureDetail failureDetail;
+
+  public UserExecException(FailureDetail failureDetail) {
+    super(failureDetail.getMessage());
+    this.failureDetail = failureDetail;
   }
 
-  public UserExecException(String message, Throwable cause) {
-    super(message, cause);
+  public UserExecException(Throwable cause, FailureDetail failureDetail) {
+    super(failureDetail.getMessage(), cause);
+    this.failureDetail = failureDetail;
   }
 
   @Override
-  public ActionExecutionException toActionExecutionException(String messagePrefix,
-        boolean verboseFailures, Action action) {
-    String message = messagePrefix + " failed";
+  public ActionExecutionException toActionExecutionException(
+      String messagePrefix, boolean verboseFailures, Action action) {
+    String message = String.format("%s failed: %s", messagePrefix, getMessage());
+    FailureDetail failureDetailWithPrefix = failureDetail.toBuilder().setMessage(message).build();
     return new ActionExecutionException(
-        message + ": " + getMessage(), this, action, isCatastrophic());
+        message, this, action, isCatastrophic(), DetailedExitCode.of(failureDetailWithPrefix));
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BUILD b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
index abd3dba..b9598c2 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
@@ -1326,6 +1326,7 @@
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
         "//src/main/java/com/google/devtools/build/lib/syntax:evaluator",
         "//src/main/java/com/google/devtools/build/lib/util",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
     ],
 )
@@ -1978,6 +1979,7 @@
         "//src/main/java/com/google/devtools/build/lib/concurrent",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
         "//src/main/java/com/google/devtools/build/lib/util",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
     ],
 )
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/ParameterFileWriteAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/ParameterFileWriteAction.java
index 5ca54b0..92162aa 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/actions/ParameterFileWriteAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/ParameterFileWriteAction.java
@@ -18,6 +18,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
 import com.google.devtools.build.lib.actions.ActionKeyContext;
@@ -34,6 +35,9 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.Spawn;
+import com.google.devtools.build.lib.server.FailureDetails.Spawn.Code;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization;
 import com.google.devtools.build.lib.syntax.EvalException;
@@ -138,7 +142,12 @@
       ArtifactExpander artifactExpander = Preconditions.checkNotNull(ctx.getArtifactExpander());
       arguments = commandLine.arguments(artifactExpander);
     } catch (CommandLineExpansionException e) {
-      throw new UserExecException(e);
+      throw new UserExecException(
+          e,
+          FailureDetail.newBuilder()
+              .setMessage(Strings.nullToEmpty(e.getMessage()))
+              .setSpawn(Spawn.newBuilder().setCode(Code.COMMAND_LINE_EXPANSION_FAILURE))
+              .build());
     }
     return new ParamFileWriter(arguments, type);
   }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java
index 474822d..c4abbeb 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/StarlarkAction.java
@@ -170,10 +170,11 @@
           .toPath(unusedInputsListArtifact)
           .getInputStream();
     } catch (FileNotFoundException e) {
-      throw new UserExecException(
+      String message =
           "Action did not create expected output file listing unused inputs: "
-              + unusedInputsListArtifact.getExecPathString(),
-          e);
+              + unusedInputsListArtifact.getExecPathString();
+      throw new UserExecException(
+          e, createFailureDetail(message, Code.UNUSED_INPUT_LIST_FILE_NOT_FOUND));
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/extra/ExtraActionInfoFileWriteAction.java b/src/main/java/com/google/devtools/build/lib/analysis/extra/ExtraActionInfoFileWriteAction.java
index 5c1263c..7b86e16 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/extra/ExtraActionInfoFileWriteAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/extra/ExtraActionInfoFileWriteAction.java
@@ -14,6 +14,7 @@
 package com.google.devtools.build.lib.analysis.extra;
 
 import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
 import com.google.devtools.build.lib.actions.Action;
 import com.google.devtools.build.lib.actions.ActionExecutionContext;
 import com.google.devtools.build.lib.actions.ActionKeyContext;
@@ -28,6 +29,9 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.Spawn;
+import com.google.devtools.build.lib.server.FailureDetails.Spawn.Code;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.util.Fingerprint;
 
@@ -62,7 +66,12 @@
       return new ProtoDeterministicWriter(
           shadowedAction.getExtraActionInfo(ctx.getActionKeyContext()).build());
     } catch (CommandLineExpansionException e) {
-      throw new UserExecException(e);
+      throw new UserExecException(
+          e,
+          FailureDetail.newBuilder()
+              .setMessage(Strings.nullToEmpty(e.getMessage()))
+              .setSpawn(Spawn.newBuilder().setCode(Code.COMMAND_LINE_EXPANSION_FAILURE))
+              .build());
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/platform/BUILD b/src/main/java/com/google/devtools/build/lib/analysis/platform/BUILD
index eed1d3a..90481d9 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/platform/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/analysis/platform/BUILD
@@ -42,6 +42,7 @@
     deps = [
         "//src/main/java/com/google/devtools/build/lib/actions",
         "//src/main/java/com/google/devtools/build/lib/remote/options",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
         "//third_party:jsr305",
         "//third_party/protobuf:protobuf_java",
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/platform/PlatformUtils.java b/src/main/java/com/google/devtools/build/lib/analysis/platform/PlatformUtils.java
index 78dd897..621a598 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/platform/PlatformUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/platform/PlatformUtils.java
@@ -22,6 +22,9 @@
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.remote.options.RemoteOptions;
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.Spawn.Code;
 import com.google.protobuf.TextFormat;
 import com.google.protobuf.TextFormat.ParseException;
 import java.util.Comparator;
@@ -84,11 +87,12 @@
         TextFormat.getParser()
             .merge(spawn.getExecutionPlatform().remoteExecutionProperties(), platformBuilder);
       } catch (ParseException e) {
-        throw new UserExecException(
+        String message =
             String.format(
                 "Failed to parse remote_execution_properties from platform %s",
-                spawn.getExecutionPlatform().label()),
-            e);
+                spawn.getExecutionPlatform().label());
+        throw new UserExecException(
+            e, createFailureDetail(message, Code.INVALID_REMOTE_EXECUTION_PROPERTIES));
       }
     } else {
       for (Map.Entry<String, String> property : defaultExecProperties.entrySet()) {
@@ -100,4 +104,11 @@
     sortPlatformProperties(platformBuilder);
     return platformBuilder.build();
   }
+
+  private static FailureDetail createFailureDetail(String message, Code detailedCode) {
+    return FailureDetail.newBuilder()
+        .setMessage(message)
+        .setSpawn(FailureDetails.Spawn.newBuilder().setCode(detailedCode))
+        .build();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
index fd3ada0..e79d8d3 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
@@ -56,6 +56,7 @@
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
+import com.google.devtools.build.lib.server.FailureDetails.TestAction;
 import com.google.devtools.build.lib.util.Fingerprint;
 import com.google.devtools.build.lib.util.LoggingUtil;
 import com.google.devtools.build.lib.util.Pair;
@@ -1165,7 +1166,8 @@
       testRunnerSpawn.finalizeTest(result, failedAttempts);
 
       if (!keepGoing && testResult != TestAttemptResult.Result.PASSED) {
-        throw new TestExecException("Test failed: aborting");
+        throw new TestExecException(
+            "Test failed: aborting", TestAction.Code.NO_KEEP_GOING_TEST_FAILURE);
       }
       return ActionContinuationOrResult.of(ActionResult.create(spawnResults));
     }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestStrategy.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestStrategy.java
index 6b49745..b9fc71b 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestStrategy.java
@@ -16,6 +16,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -39,6 +40,9 @@
 import com.google.devtools.build.lib.exec.TestLogHelper;
 import com.google.devtools.build.lib.exec.TestXmlOutputParser;
 import com.google.devtools.build.lib.exec.TestXmlOutputParserException;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.TestAction;
+import com.google.devtools.build.lib.server.FailureDetails.TestAction.Code;
 import com.google.devtools.build.lib.util.Fingerprint;
 import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.util.io.OutErr;
@@ -141,7 +145,12 @@
     try {
       return expandedArgsFromAction(testAction);
     } catch (CommandLineExpansionException e) {
-      throw new UserExecException(e);
+      throw new UserExecException(
+          e,
+          FailureDetail.newBuilder()
+              .setMessage(Strings.nullToEmpty(e.getMessage()))
+              .setTestAction(TestAction.newBuilder().setCode(Code.COMMAND_LINE_EXPANSION_FAILURE))
+              .build());
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java
index 214969c..15a422f 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java
@@ -28,6 +28,9 @@
 import com.google.devtools.build.lib.packages.TestSize;
 import com.google.devtools.build.lib.packages.TestTimeout;
 import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.TestAction;
+import com.google.devtools.build.lib.server.FailureDetails.TestAction.Code;
 import java.util.List;
 import java.util.Map;
 
@@ -145,10 +148,11 @@
         String cpus = ExecutionRequirements.CPU.parseIfMatches(tag);
         if (cpus != null) {
           if (testResourcesFromTag != null) {
-            throw new UserExecException(
+            String message =
                 String.format(
                     "%s has more than one '%s' tag, but duplicate tags aren't allowed",
-                    label, ExecutionRequirements.CPU.userFriendlyName()));
+                    label, ExecutionRequirements.CPU.userFriendlyName());
+            throw new UserExecException(createFailureDetail(message, Code.DUPLICATE_CPU_TAGS));
           }
           testResourcesFromTag =
               ResourceSet.create(
@@ -157,19 +161,27 @@
                   testResourcesFromSize.getLocalTestCount());
         }
       } catch (ValidationException e) {
-        throw new UserExecException(
+        String message =
             String.format(
                 "%s has a '%s' tag, but its value '%s' didn't pass validation: %s",
                 label,
                 ExecutionRequirements.CPU.userFriendlyName(),
                 e.getTagValue(),
-                e.getMessage()));
+                e.getMessage());
+        throw new UserExecException(createFailureDetail(message, Code.INVALID_CPU_TAG));
       }
     }
 
     return testResourcesFromTag != null ? testResourcesFromTag : testResourcesFromSize;
   }
 
+  private static FailureDetail createFailureDetail(String message, Code detailedCode) {
+    return FailureDetail.newBuilder()
+        .setMessage(message)
+        .setTestAction(TestAction.newBuilder().setCode(detailedCode))
+        .build();
+  }
+
   /**
    * Returns a map of execution info. See {@link
    * com.google.devtools.build.lib.actions.Spawn#getExecutionInfo}.
diff --git a/src/main/java/com/google/devtools/build/lib/dynamic/LegacyDynamicSpawnStrategy.java b/src/main/java/com/google/devtools/build/lib/dynamic/LegacyDynamicSpawnStrategy.java
index 69379a1..f22c120 100644
--- a/src/main/java/com/google/devtools/build/lib/dynamic/LegacyDynamicSpawnStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/dynamic/LegacyDynamicSpawnStrategy.java
@@ -16,6 +16,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -243,9 +244,13 @@
                   }));
     } catch (ExecutionException e) {
       Throwables.propagateIfPossible(e.getCause(), InterruptedException.class);
-      // DynamicExecutionCallable.callImpl only declares InterruptedException, so this should never
+      // DynamicExecutionCallable.call only declares InterruptedException, so this should never
       // happen.
-      exceptionDuringExecution = new UserExecException(e.getCause());
+      exceptionDuringExecution =
+          new UserExecException(
+              e.getCause(),
+              createFailureDetail(
+                  Strings.nullToEmpty(e.getCause().getMessage()), Code.RUN_FAILURE));
     } finally {
       bothTasksFinished.arriveAndAwaitAdvance();
       if (dynamicExecutionResult.execException() != null) {
@@ -273,7 +278,10 @@
       String strategyName = winningStrategy.name().toLowerCase();
       if (exceptionDuringExecution == null) {
         throw new UserExecException(
-            String.format("Could not move action logs from %s execution", strategyName), e);
+            e,
+            createFailureDetail(
+                String.format("Could not move action logs from %s execution", strategyName),
+                Code.ACTION_LOG_MOVE_FAILURE));
       } else {
         actionExecutionContext
             .getEventHandler()
@@ -442,7 +450,11 @@
         Throwables.throwIfInstanceOf(e, InterruptedException.class);
         return DynamicExecutionResult.create(
             strategyIdentifier,
-            fileOutErr, e instanceof ExecException ? (ExecException) e : new UserExecException(e),
+            fileOutErr,
+            e instanceof ExecException
+                ? (ExecException) e
+                : new UserExecException(
+                    e, createFailureDetail(Strings.nullToEmpty(e.getMessage()), Code.RUN_FAILURE)),
             /*spawnResults=*/ ImmutableList.of());
       } finally {
         try {
diff --git a/src/main/java/com/google/devtools/build/lib/exec/BUILD b/src/main/java/com/google/devtools/build/lib/exec/BUILD
index 355bc3a..9251478 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/exec/BUILD
@@ -287,6 +287,7 @@
     deps = [
         ":spawn_strategy_registry",
         "//src/main/java/com/google/devtools/build/lib/actions",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
     ],
 )
diff --git a/src/main/java/com/google/devtools/build/lib/exec/SpawnStrategyResolver.java b/src/main/java/com/google/devtools/build/lib/exec/SpawnStrategyResolver.java
index 902206c..da5a9ca 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/SpawnStrategyResolver.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/SpawnStrategyResolver.java
@@ -23,6 +23,9 @@
 import com.google.devtools.build.lib.actions.SpawnResult;
 import com.google.devtools.build.lib.actions.SpawnStrategy;
 import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.Spawn.Code;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -91,13 +94,18 @@
             .collect(Collectors.toList());
 
     if (strategies.isEmpty()) {
-      throw new UserExecException(
+      String message =
           String.format(
               "No usable spawn strategy found for spawn with mnemonic %s.  Your"
                   + " --spawn_strategy, --genrule_strategy and/or --strategy flags are probably too"
                   + " strict. Visit https://github.com/bazelbuild/bazel/issues/7480 for"
                   + " migration advice",
-              spawn.getMnemonic()));
+              spawn.getMnemonic());
+      throw new UserExecException(
+          FailureDetail.newBuilder()
+              .setMessage(message)
+              .setSpawn(FailureDetails.Spawn.newBuilder().setCode(Code.NO_USABLE_STRATEGY_FOUND))
+              .build());
     }
 
     return strategies;
diff --git a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
index c11ca37..e0d24d1 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java
@@ -47,6 +47,7 @@
 import com.google.devtools.build.lib.collect.nestedset.Order;
 import com.google.devtools.build.lib.events.Reporter;
 import com.google.devtools.build.lib.server.FailureDetails.Execution.Code;
+import com.google.devtools.build.lib.server.FailureDetails.TestAction;
 import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.vfs.FileStatus;
@@ -95,7 +96,9 @@
   public TestRunnerSpawn createTestRunnerSpawn(
       TestRunnerAction action, ActionExecutionContext actionExecutionContext) throws ExecException {
     if (action.getExecutionSettings().getInputManifest() == null) {
-      throw new TestExecException("cannot run local tests with --nobuild_runfile_manifests");
+      throw new TestExecException(
+          "cannot run local tests with --nobuild_runfile_manifests",
+          TestAction.Code.LOCAL_TEST_PREREQ_UNMET);
     }
     Path execRoot = actionExecutionContext.getExecRoot();
     ArtifactPathResolver pathResolver = actionExecutionContext.getPathResolver();
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java
index 92d1e32..afc1562 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java
@@ -356,12 +356,9 @@
         ExecException execEx =
             new EnvironmentalExecException(
                 ioEx,
-                FailureDetail.newBuilder()
-                    .setMessage("Failed to delete output files after incomplete download")
-                    .setRemoteExecution(
-                        RemoteExecution.newBuilder()
-                            .setCode(Code.INCOMPLETE_OUTPUT_DOWNLOAD_CLEANUP_FAILURE))
-                    .build());
+                createFailureDetail(
+                    "Failed to delete output files after incomplete download",
+                    Code.INCOMPLETE_OUTPUT_DOWNLOAD_CLEANUP_FAILURE));
         execEx.addSuppressed(e);
         throw execEx;
       }
@@ -968,12 +965,13 @@
 
     private void illegalOutput(Path what) throws ExecException {
       String kind = what.isSymbolicLink() ? "symbolic link" : "special file";
-      throw new UserExecException(
+      String message =
           String.format(
               "Output %s is a %s. Only regular files and directories may be "
                   + "uploaded to a remote cache. "
                   + "Change the file type or use --remote_allow_symlink_upload.",
-              what.relativeTo(execRoot), kind));
+              what.relativeTo(execRoot), kind);
+      throw new UserExecException(createFailureDetail(message, Code.ILLEGAL_OUTPUT));
     }
   }
 
@@ -983,6 +981,13 @@
     cacheProtocol.close();
   }
 
+  private static FailureDetail createFailureDetail(String message, Code detailedCode) {
+    return FailureDetail.newBuilder()
+        .setMessage(message)
+        .setRemoteExecution(RemoteExecution.newBuilder().setCode(detailedCode))
+        .build();
+  }
+
   /**
    * Creates an {@link OutputStream} that isn't actually opened until the first data is written.
    * This is useful to only have as many open file descriptors as necessary at a time to avoid
diff --git a/src/main/java/com/google/devtools/build/lib/remote/options/BUILD b/src/main/java/com/google/devtools/build/lib/remote/options/BUILD
index 5a714d7..1e7be66 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/options/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/remote/options/BUILD
@@ -18,6 +18,7 @@
         "//src/main/java/com/google/devtools/build/lib/util",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
         "//src/main/java/com/google/devtools/common/options",
+        "//src/main/protobuf:failure_details_java_proto",
         "//third_party:guava",
         "//third_party/protobuf:protobuf_java",
         "@remoteapis//:build_bazel_remote_execution_v2_remote_execution_java_proto",
diff --git a/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java b/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
index d31bfa7..0c400d1 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
@@ -19,6 +19,9 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.RemoteExecution;
+import com.google.devtools.build.lib.server.FailureDetails.RemoteExecution.Code;
 import com.google.devtools.build.lib.util.OptionsUtils;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.common.options.Converters;
@@ -454,9 +457,11 @@
 
     if (hasExecProperties && hasPlatformProperties) {
       throw new UserExecException(
-          "Setting both --remote_default_platform_properties and "
-              + "--remote_default_exec_properties is not allowed. Prefer setting "
-              + "--remote_default_exec_properties.");
+          createFailureDetail(
+              "Setting both --remote_default_platform_properties and "
+                  + "--remote_default_exec_properties is not allowed. Prefer setting "
+                  + "--remote_default_exec_properties.",
+              Code.INVALID_EXEC_AND_PLATFORM_PROPERTIES));
     }
 
     if (hasExecProperties) {
@@ -470,10 +475,11 @@
         TextFormat.getParser().merge(remoteDefaultPlatformProperties, builder);
         platform = builder.build();
       } catch (ParseException e) {
-        throw new UserExecException(
+        String message =
             "Failed to parse --remote_default_platform_properties "
-                + remoteDefaultPlatformProperties,
-            e);
+                + remoteDefaultPlatformProperties;
+        throw new UserExecException(
+            e, createFailureDetail(message, Code.REMOTE_DEFAULT_PLATFORM_PROPERTIES_PARSE_FAILURE));
       }
 
       ImmutableSortedMap.Builder<String, String> builder = ImmutableSortedMap.naturalOrder();
@@ -485,4 +491,11 @@
 
     return ImmutableSortedMap.of();
   }
+
+  private static FailureDetail createFailureDetail(String message, Code detailedCode) {
+    return FailureDetail.newBuilder()
+        .setMessage(message)
+        .setRemoteExecution(RemoteExecution.newBuilder().setCode(detailedCode))
+        .build();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java
index 37078d0..4d53e00 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/IncludeScanning.java
@@ -143,8 +143,10 @@
           continue;
         }
         throw new UserExecException(
-            "illegal absolute path to include file: "
-                + actionExecutionContext.getInputPath(included));
+            createFailureDetail(
+                "illegal absolute path to include file: "
+                    + actionExecutionContext.getInputPath(included),
+                Code.ILLEGAL_ABSOLUTE_PATH));
       }
       if (included.hasParent() && included.getParent().isTreeArtifact()) {
         // Note that this means every file in the TreeArtifact becomes an input to the action, and
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
index 4a667a4..886f82c 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/AbstractSandboxSpawnRunner.java
@@ -36,6 +36,9 @@
 import com.google.devtools.build.lib.profiler.Profiler;
 import com.google.devtools.build.lib.profiler.SilentCloseable;
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.Sandbox;
+import com.google.devtools.build.lib.server.FailureDetails.Sandbox.Code;
 import com.google.devtools.build.lib.shell.ExecutionStatistics;
 import com.google.devtools.build.lib.shell.Subprocess;
 import com.google.devtools.build.lib.shell.SubprocessBuilder;
@@ -86,7 +89,10 @@
       SandboxedSpawn sandbox = prepareSpawn(spawn, context);
       return runSpawn(spawn, sandbox, context);
     } catch (IOException e) {
-      throw new UserExecException("I/O exception during sandboxed execution", e);
+      FailureDetail failureDetail =
+          createFailureDetail(
+              "I/O exception during sandboxed execution", Code.EXECUTION_IO_EXCEPTION);
+      throw new UserExecException(e, failureDetail);
     }
   }
 
@@ -347,4 +353,11 @@
       }
     }
   }
+
+  static FailureDetail createFailureDetail(String message, Code detailedCode) {
+    return FailureDetail.newBuilder()
+        .setMessage(message)
+        .setSandbox(Sandbox.newBuilder().setCode(detailedCode))
+        .build();
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/DockerSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/DockerSandboxedSpawnRunner.java
index 39da19d..f319b53 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/DockerSandboxedSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/DockerSandboxedSpawnRunner.java
@@ -37,6 +37,7 @@
 import com.google.devtools.build.lib.runtime.ProcessWrapper;
 import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxInputs;
 import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxOutputs;
+import com.google.devtools.build.lib.server.FailureDetails.Sandbox.Code;
 import com.google.devtools.build.lib.shell.Command;
 import com.google.devtools.build.lib.shell.CommandException;
 import com.google.devtools.build.lib.util.OS;
@@ -232,16 +233,21 @@
     String baseImageName = dockerContainerFromSpawn(spawn).orElse(this.defaultImage);
     if (baseImageName.isEmpty()) {
       throw new UserExecException(
-          String.format(
-              "Cannot execute %s mnemonic with Docker, because no "
-                  + "image could be found in the remote_execution_properties of the platform and "
-                  + "no default image was set via --experimental_docker_image",
-              spawn.getMnemonic()));
+          createFailureDetail(
+              String.format(
+                  "Cannot execute %s mnemonic with Docker, because no image could be found in the"
+                      + " remote_execution_properties of the platform and no default image was set"
+                      + " via --experimental_docker_image",
+                  spawn.getMnemonic()),
+              Code.NO_DOCKER_IMAGE));
     }
 
     String customizedImageName = getOrCreateCustomizedImage(baseImageName);
     if (customizedImageName == null) {
-      throw new UserExecException("Could not prepare Docker image for execution");
+      throw new UserExecException(
+          createFailureDetail(
+              "Could not prepare Docker image for execution",
+              Code.DOCKER_IMAGE_PREPARATION_FAILURE));
     }
 
     DockerCommandLineBuilder cmdLine = new DockerCommandLineBuilder();
@@ -369,8 +375,8 @@
     try {
       cmd.executeAsync(stdIn, stdOut, stdErr, Command.KILL_SUBPROCESS_ON_INTERRUPT).get();
     } catch (CommandException e) {
-      throw new UserExecException(
-          "Running command " + cmd.toDebugString() + " failed: " + stdErr, e);
+      String message = String.format("Running command %s failed: %s", cmd.toDebugString(), stdErr);
+      throw new UserExecException(e, createFailureDetail(message, Code.DOCKER_COMMAND_FAILURE));
     }
     return stdOut.toString().trim();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
index 3332796..114063a 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
@@ -33,6 +33,7 @@
 import com.google.devtools.build.lib.runtime.CommandEnvironment;
 import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxInputs;
 import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxOutputs;
+import com.google.devtools.build.lib.server.FailureDetails.Sandbox.Code;
 import com.google.devtools.build.lib.shell.Command;
 import com.google.devtools.build.lib.shell.CommandException;
 import com.google.devtools.build.lib.util.OS;
@@ -277,7 +278,9 @@
         bindMounts.put(mountTarget, mountSource);
       } catch (IllegalArgumentException e) {
         throw new UserExecException(
-            String.format("Error occurred when analyzing bind mount pairs. %s", e.getMessage()));
+            createFailureDetail(
+                String.format("Error occurred when analyzing bind mount pairs. %s", e.getMessage()),
+                Code.BIND_MOUNT_ANALYSIS_FAILURE));
       }
     }
     for (Path inaccessiblePath : getInaccessiblePaths()) {
@@ -305,7 +308,10 @@
       final Path target = bindMount.getKey();
       // Mount source should exist in the file system
       if (!source.exists()) {
-        throw new UserExecException(String.format("Mount source '%s' does not exist.", source));
+        throw new UserExecException(
+            createFailureDetail(
+                String.format("Mount source '%s' does not exist.", source),
+                Code.MOUNT_SOURCE_DOES_NOT_EXIST));
       }
       // If target exists, but is not of the same type as the source, then we cannot mount it.
       if (target.exists()) {
@@ -316,18 +322,22 @@
         if (!(areBothDirectories || areBothFiles)) {
           // Source and target are not of the same type; we cannot mount it.
           throw new UserExecException(
-              String.format(
-                  "Mount target '%s' is not of the same type as mount source '%s'.",
-                  target, source));
+              createFailureDetail(
+                  String.format(
+                      "Mount target '%s' is not of the same type as mount source '%s'.",
+                      target, source),
+                  Code.MOUNT_SOURCE_TARGET_TYPE_MISMATCH));
         }
       } else {
         // Mount target should exist in the file system
         throw new UserExecException(
-            String.format(
-                "Mount target '%s' does not exist. Bazel only supports bind mounting on top of "
-                    + "existing files/directories. Please create an empty file or directory at "
-                    + "the mount target path according to the type of mount source.",
-                target));
+            createFailureDetail(
+                String.format(
+                    "Mount target '%s' does not exist. Bazel only supports bind mounting on top of "
+                        + "existing files/directories. Please create an empty file or directory at "
+                        + "the mount target path according to the type of mount source.",
+                    target),
+                Code.MOUNT_TARGET_DOES_NOT_EXIST));
       }
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/worker/BUILD b/src/main/java/com/google/devtools/build/lib/worker/BUILD
index 27f6381..0d3a13f 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/worker/BUILD
@@ -30,6 +30,7 @@
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
         "//src/main/java/com/google/devtools/common/options",
+        "//src/main/protobuf:failure_details_java_proto",
         "//src/main/protobuf:worker_protocol_java_proto",
         "//third_party:apache_commons_pool2",
         "//third_party:auto_value",
diff --git a/src/main/java/com/google/devtools/build/lib/worker/WorkerMultiplexerManager.java b/src/main/java/com/google/devtools/build/lib/worker/WorkerMultiplexerManager.java
index 028f031..d3582d4 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/WorkerMultiplexerManager.java
+++ b/src/main/java/com/google/devtools/build/lib/worker/WorkerMultiplexerManager.java
@@ -15,11 +15,14 @@
 package com.google.devtools.build.lib.worker;
 
 import com.google.devtools.build.lib.actions.UserExecException;
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.Worker.Code;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Semaphore;
 
-/** A manager to instantiate and distroy multiplexers. */
+/** A manager to instantiate and destroy multiplexers. */
 public class WorkerMultiplexerManager {
   /**
    * There should only be one WorkerMultiplexer corresponding to workers with the same mnemonic. If
@@ -67,12 +70,8 @@
         multiplexerInstance.remove(workerHash);
       }
     } catch (Exception e) {
-      throw new UserExecException(
-          ErrorMessage.builder()
-              .message("NullPointerException while accessing non-existent multiplexer instance.")
-              .exception(e)
-              .build()
-              .toString());
+      String message = "NullPointerException while accessing non-existent multiplexer instance.";
+      throw createUserExecException(e, message, Code.MULTIPLEXER_INSTANCE_REMOVAL_FAILURE);
     } finally {
       semMultiplexer.release();
     }
@@ -82,12 +81,8 @@
     try {
       return multiplexerInstance.get(workerHash).getWorkerMultiplexer();
     } catch (NullPointerException e) {
-      throw new UserExecException(
-          ErrorMessage.builder()
-              .message("NullPointerException while accessing non-existent multiplexer instance.")
-              .exception(e)
-              .build()
-              .toString());
+      String message = "NullPointerException while accessing non-existent multiplexer instance.";
+      throw createUserExecException(e, message, Code.MULTIPLEXER_DOES_NOT_EXIST);
     }
   }
 
@@ -95,12 +90,8 @@
     try {
       return multiplexerInstance.get(workerHash).getRefCount();
     } catch (NullPointerException e) {
-      throw new UserExecException(
-          ErrorMessage.builder()
-              .message("NullPointerException while accessing non-existent multiplexer instance.")
-              .exception(e)
-              .build()
-              .toString());
+      String message = "NullPointerException while accessing non-existent multiplexer instance.";
+      throw createUserExecException(e, message, Code.MULTIPLEXER_DOES_NOT_EXIST);
     }
   }
 
@@ -108,6 +99,15 @@
     return multiplexerInstance.keySet().size();
   }
 
+  private static UserExecException createUserExecException(
+      Exception e, String message, Code detailedCode) {
+    return new UserExecException(
+        FailureDetail.newBuilder()
+            .setMessage(ErrorMessage.builder().message(message).exception(e).build().toString())
+            .setWorker(FailureDetails.Worker.newBuilder().setCode(detailedCode))
+            .build());
+  }
+
   /** Contains the WorkerMultiplexer instance and reference count */
   static class InstanceInfo {
     private WorkerMultiplexer workerMultiplexer;
diff --git a/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java
index f4f8631..ee2f6ea 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java
@@ -44,6 +44,9 @@
 import com.google.devtools.build.lib.sandbox.SandboxHelpers;
 import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxInputs;
 import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxOutputs;
+import com.google.devtools.build.lib.server.FailureDetails;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.server.FailureDetails.Worker.Code;
 import com.google.devtools.build.lib.util.io.FileOutErr;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -149,8 +152,9 @@
   private SpawnResult actuallyExec(Spawn spawn, SpawnExecutionContext context)
       throws ExecException, IOException, InterruptedException {
     if (spawn.getToolFiles().isEmpty()) {
-      throw new UserExecException(
-          String.format(ERROR_MESSAGE_PREFIX + REASON_NO_TOOLS, spawn.getMnemonic()));
+      throw createUserExecException(
+          String.format(ERROR_MESSAGE_PREFIX + REASON_NO_TOOLS, spawn.getMnemonic()),
+          Code.NO_TOOLS);
     }
 
     runfilesTreeUpdater.updateRunfilesDirectory(
@@ -239,8 +243,9 @@
     }
 
     if (flagFiles.isEmpty()) {
-      throw new UserExecException(
-          String.format(ERROR_MESSAGE_PREFIX + REASON_NO_FLAGFILE, spawn.getMnemonic()));
+      throw createUserExecException(
+          String.format(ERROR_MESSAGE_PREFIX + REASON_NO_FLAGFILE, spawn.getMnemonic()),
+          Code.NO_FLAGFILE);
     }
 
     return workerArgs
@@ -333,12 +338,8 @@
       try {
         inputFiles.materializeVirtualInputs(execRoot);
       } catch (IOException e) {
-        throw new UserExecException(
-            ErrorMessage.builder()
-                .message("IOException while materializing virtual inputs:")
-                .exception(e)
-                .build()
-                .toString());
+        String message = "IOException while materializing virtual inputs:";
+        throw createUserExecException(e, message, Code.VIRTUAL_INPUT_MATERIALIZATION_FAILURE);
       }
 
       try {
@@ -346,23 +347,15 @@
         request =
             createWorkRequest(spawn, context, flagFiles, inputFileCache, worker.getWorkerId());
       } catch (IOException e) {
-        throw new UserExecException(
-            ErrorMessage.builder()
-                .message("IOException while borrowing a worker from the pool:")
-                .exception(e)
-                .build()
-                .toString());
+        String message = "IOException while borrowing a worker from the pool:";
+        throw createUserExecException(e, message, Code.BORROW_FAILURE);
       }
 
       try {
         context.prefetchInputs();
       } catch (IOException e) {
-        throw new UserExecException(
-            ErrorMessage.builder()
-                .message("IOException while prefetching for worker:")
-                .exception(e)
-                .build()
-                .toString());
+        String message = "IOException while prefetching for worker:";
+        throw createUserExecException(e, message, Code.PREFETCH_FAILURE);
       }
 
       try (ResourceHandle handle =
@@ -371,19 +364,20 @@
         try {
           worker.prepareExecution(inputFiles, outputs, key.getWorkerFilesWithHashes().keySet());
         } catch (IOException e) {
-          throw new UserExecException(
+          String message =
               ErrorMessage.builder()
                   .message("IOException while preparing the execution environment of a worker:")
                   .logFile(worker.getLogFile())
                   .exception(e)
                   .build()
-                  .toString());
+                  .toString();
+          throw createUserExecException(message, Code.PREPARE_FAILURE);
         }
 
         try {
           worker.putRequest(request);
         } catch (IOException e) {
-          throw new UserExecException(
+          String message =
               ErrorMessage.builder()
                   .message(
                       "Worker process quit or closed its stdin stream when we tried to send a"
@@ -391,7 +385,8 @@
                   .logFile(worker.getLogFile())
                   .exception(e)
                   .build()
-                  .toString());
+                  .toString();
+          throw createUserExecException(message, Code.REQUEST_FAILURE);
         }
 
         try {
@@ -401,7 +396,7 @@
           // to stdout - it's probably a stack trace or some kind of error message that will help
           // the user figure out why the compiler is failing.
           String recordingStreamMessage = worker.getRecordingStreamMessage();
-          throw new UserExecException(
+          String message =
               ErrorMessage.builder()
                   .message(
                       "Worker process returned an unparseable WorkResponse!\n\n"
@@ -411,32 +406,35 @@
                   .logText(recordingStreamMessage)
                   .exception(e)
                   .build()
-                  .toString());
+                  .toString();
+          throw createUserExecException(message, Code.PARSE_RESPONSE_FAILURE);
         }
       }
 
       if (response == null) {
-        throw new UserExecException(
+        String message =
             ErrorMessage.builder()
                 .message("Worker process did not return a WorkResponse:")
                 .logFile(worker.getLogFile())
                 .logSizeLimit(4096)
                 .build()
-                .toString());
+                .toString();
+        throw createUserExecException(message, Code.NO_RESPONSE);
       }
 
       try {
         context.lockOutputFiles();
         worker.finishExecution(execRoot);
       } catch (IOException e) {
-        throw new UserExecException(
+        String message =
             ErrorMessage.builder()
                 .message("IOException while finishing worker execution:")
                 .exception(e)
                 .build()
-                .toString());
+                .toString();
+        throw createUserExecException(message, Code.FINISH_FAILURE);
       }
-    } catch (ExecException e) {
+    } catch (UserExecException e) {
       if (worker != null) {
         try {
           workers.invalidateObject(key, worker);
@@ -455,4 +453,18 @@
 
     return response;
   }
+
+  private static UserExecException createUserExecException(
+      IOException e, String message, Code detailedCode) {
+    return createUserExecException(
+        ErrorMessage.builder().message(message).exception(e).build().toString(), detailedCode);
+  }
+
+  private static UserExecException createUserExecException(String message, Code detailedCode) {
+    return new UserExecException(
+        FailureDetail.newBuilder()
+            .setMessage(message)
+            .setWorker(FailureDetails.Worker.newBuilder().setCode(detailedCode))
+            .build());
+  }
 }
diff --git a/src/main/protobuf/failure_details.proto b/src/main/protobuf/failure_details.proto
index 0530743..6147481 100644
--- a/src/main/protobuf/failure_details.proto
+++ b/src/main/protobuf/failure_details.proto
@@ -139,6 +139,8 @@
     SymlinkAction symlink_action = 167;
     CppLink cpp_link = 168;
     LtoAction lto_action = 169;
+    TestAction test_action = 172;
+    Worker worker = 173;
   }
 
   reserved 102; // For internal use
@@ -199,6 +201,9 @@
     REMOTE_CACHE_FAILED = 6 [(metadata) = { exit_code: 34 }];
     COMMAND_LINE_EXPANSION_FAILURE = 7 [(metadata) = { exit_code: 1 }];
     EXEC_IO_EXCEPTION = 8 [(metadata) = { exit_code: 36 }];
+    INVALID_TIMEOUT = 9 [(metadata) = { exit_code: 1 }];
+    INVALID_REMOTE_EXECUTION_PROPERTIES = 10 [(metadata) = { exit_code: 1 }];
+    NO_USABLE_STRATEGY_FOUND = 11 [(metadata) = { exit_code: 1 }];
   }
   Code code = 1;
 
@@ -345,6 +350,10 @@
         [(metadata) = { exit_code: 2 }];
     INCOMPLETE_OUTPUT_DOWNLOAD_CLEANUP_FAILURE = 13
         [(metadata) = { exit_code: 36 }];
+    REMOTE_DEFAULT_PLATFORM_PROPERTIES_PARSE_FAILURE = 14
+        [(metadata) = { exit_code: 1 }];
+    ILLEGAL_OUTPUT = 15 [(metadata) = { exit_code: 1 }];
+    INVALID_EXEC_AND_PLATFORM_PROPERTIES = 16 [(metadata) = { exit_code: 1 }];
   }
 
   Code code = 1;
@@ -627,6 +636,14 @@
   enum Code {
     SANDBOX_FAILURE_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
     INITIALIZATION_FAILURE = 1 [(metadata) = { exit_code: 36 }];
+    EXECUTION_IO_EXCEPTION = 2 [(metadata) = { exit_code: 1 }];
+    DOCKER_COMMAND_FAILURE = 3 [(metadata) = { exit_code: 1 }];
+    NO_DOCKER_IMAGE = 4 [(metadata) = { exit_code: 1 }];
+    DOCKER_IMAGE_PREPARATION_FAILURE = 5 [(metadata) = { exit_code: 1 }];
+    BIND_MOUNT_ANALYSIS_FAILURE = 6 [(metadata) = { exit_code: 1 }];
+    MOUNT_SOURCE_DOES_NOT_EXIST = 7 [(metadata) = { exit_code: 1 }];
+    MOUNT_SOURCE_TARGET_TYPE_MISMATCH = 8 [(metadata) = { exit_code: 1 }];
+    MOUNT_TARGET_DOES_NOT_EXIST = 9 [(metadata) = { exit_code: 1 }];
   }
 
   Code code = 1;
@@ -639,6 +656,7 @@
     SCANNING_IO_EXCEPTION = 2 [(metadata) = { exit_code: 36 }];
     INCLUDE_HINTS_FILE_NOT_IN_PACKAGE = 3 [(metadata) = { exit_code: 36 }];
     INCLUDE_HINTS_READ_FAILURE = 4 [(metadata) = { exit_code: 36 }];
+    ILLEGAL_ABSOLUTE_PATH = 5 [(metadata) = { exit_code: 1 }];
   }
 
   Code code = 1;
@@ -882,6 +900,7 @@
   enum Code {
     STARLARK_ACTION_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
     UNUSED_INPUT_LIST_READ_FAILURE = 1 [(metadata) = { exit_code: 36 }];
+    UNUSED_INPUT_LIST_FILE_NOT_FOUND = 2 [(metadata) = { exit_code: 1 }];
   }
 
   Code code = 1;
@@ -901,6 +920,8 @@
   enum Code {
     DYNAMIC_EXECUTION_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
     XCODE_RELATED_PREREQ_UNMET = 1 [(metadata) = { exit_code: 36 }];
+    ACTION_LOG_MOVE_FAILURE = 2 [(metadata) = { exit_code: 1 }];
+    RUN_FAILURE = 3 [(metadata) = { exit_code: 1 }];
   }
 
   Code code = 1;
@@ -948,3 +969,36 @@
 
   Code code = 1;
 }
+
+message TestAction {
+  enum Code {
+    TEST_ACTION_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
+    NO_KEEP_GOING_TEST_FAILURE = 1 [(metadata) = { exit_code: 1 }];
+    LOCAL_TEST_PREREQ_UNMET = 2 [(metadata) = { exit_code: 1 }];
+    COMMAND_LINE_EXPANSION_FAILURE = 3 [(metadata) = { exit_code: 1 }];
+    DUPLICATE_CPU_TAGS = 4 [(metadata) = { exit_code: 1 }];
+    INVALID_CPU_TAG = 5 [(metadata) = { exit_code: 1 }];
+  }
+
+  Code code = 1;
+}
+
+message Worker {
+  enum Code {
+    WORKER_UNKNOWN = 0 [(metadata) = { exit_code: 37 }];
+    MULTIPLEXER_INSTANCE_REMOVAL_FAILURE = 1 [(metadata) = { exit_code: 1 }];
+    MULTIPLEXER_DOES_NOT_EXIST = 2 [(metadata) = { exit_code: 1 }];
+    NO_TOOLS = 3 [(metadata) = { exit_code: 1 }];
+    NO_FLAGFILE = 4 [(metadata) = { exit_code: 1 }];
+    VIRTUAL_INPUT_MATERIALIZATION_FAILURE = 5 [(metadata) = { exit_code: 1 }];
+    BORROW_FAILURE = 6 [(metadata) = { exit_code: 1 }];
+    PREFETCH_FAILURE = 7 [(metadata) = { exit_code: 1 }];
+    PREPARE_FAILURE = 8 [(metadata) = { exit_code: 1 }];
+    REQUEST_FAILURE = 9 [(metadata) = { exit_code: 1 }];
+    PARSE_RESPONSE_FAILURE = 10 [(metadata) = { exit_code: 1 }];
+    NO_RESPONSE = 11 [(metadata) = { exit_code: 1 }];
+    FINISH_FAILURE = 12 [(metadata) = { exit_code: 1 }];
+  }
+
+  Code code = 1;
+}
\ No newline at end of file
diff --git a/src/test/java/com/google/devtools/build/lib/dynamic/BUILD b/src/test/java/com/google/devtools/build/lib/dynamic/BUILD
index 3338aa1..f02079a 100644
--- a/src/test/java/com/google/devtools/build/lib/dynamic/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/dynamic/BUILD
@@ -28,6 +28,7 @@
         "//src/main/java/com/google/devtools/build/lib/util/io",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/common/options",
+        "//src/main/protobuf:failure_details_java_proto",
         "//src/test/java/com/google/devtools/build/lib/actions/util",
         "//src/test/java/com/google/devtools/build/lib/testutil:TestThread",
         "//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",
diff --git a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
index 244d202..d3901c0 100644
--- a/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/dynamic/DynamicSpawnStrategyTest.java
@@ -49,6 +49,9 @@
 import com.google.devtools.build.lib.exec.ExecutionOptions;
 import com.google.devtools.build.lib.exec.ModuleActionContextRegistry;
 import com.google.devtools.build.lib.exec.SpawnStrategyRegistry;
+import com.google.devtools.build.lib.server.FailureDetails.DynamicExecution;
+import com.google.devtools.build.lib.server.FailureDetails.DynamicExecution.Code;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.testutil.TestThread;
 import com.google.devtools.build.lib.testutil.TestUtils;
 import com.google.devtools.build.lib.util.AbruptExitException;
@@ -152,7 +155,7 @@
       } catch (IOException e) {
         throw new IllegalStateException(e);
       }
-      throw new UserExecException(name + " failed to execute the Spawn");
+      throw new UserExecException(createFailureDetail(name + " failed to execute the Spawn"));
     }
 
     @Override
@@ -989,7 +992,7 @@
 
     Spawn spawn = newDynamicSpawn();
     Exception e = assertThrows(expectedException.getClass(), () -> strategyAndContext.exec(spawn));
-    assertThat(e).hasMessageThat().matches(expectedException.getMessage());
+    assertThat(e).hasMessageThat().contains(expectedException.getMessage());
 
     Spawn executedSpawn = localStrategy.getExecutedSpawn();
     executedSpawn = executedSpawn == null ? remoteStrategy.getExecutedSpawn() : executedSpawn;
@@ -1013,7 +1016,9 @@
         };
 
     assertThatStrategyPropagatesException(
-        localExec, remoteExec, legacyBehavior ? new UserExecException(e) : e);
+        localExec,
+        remoteExec,
+        legacyBehavior ? new UserExecException(e, createFailureDetail("")) : e);
   }
 
   @Test
@@ -1031,7 +1036,16 @@
         };
 
     assertThatStrategyPropagatesException(
-        localExec, remoteExec, legacyBehavior ? new UserExecException(e) : e);
+        localExec,
+        remoteExec,
+        legacyBehavior ? new UserExecException(e, createFailureDetail("")) : e);
+  }
+
+  private static FailureDetail createFailureDetail(String message) {
+    return FailureDetail.newBuilder()
+        .setMessage(message)
+        .setDynamicExecution(DynamicExecution.newBuilder().setCode(Code.RUN_FAILURE))
+        .build();
   }
 
   @AutoValue