[tint] Add a diag::List to tint::Failure

Use this to describe _why_ the function failed.

Use this in most places that currently errors with std::string.
This preserves the diagnostic metadata, which would otherwise get baked into a string.

Remove Diagnostic::code. It was never used, and is just consuming memory.

Make Diagnostic use tint::Vector instead of std::vector.

Add some magic so that 'operator<<(STREAM, Failure)' can be used with unformattable SUCCESS / FAILURE types.
Simplifies test that check for success.

Change-Id: I2e474c5387c3fbe68cb6b6c48b123d2597ef82e6
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/152546
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/src/dawn/native/CompilationMessages.cpp b/src/dawn/native/CompilationMessages.cpp
index a8d1695..b91e69e 100644
--- a/src/dawn/native/CompilationMessages.cpp
+++ b/src/dawn/native/CompilationMessages.cpp
@@ -148,11 +148,7 @@
                                            fileStart + offsetInBytes, lengthInBytes)));
     }
 
-    if (diagnostic.code) {
-        mMessageStrings.push_back(std::string(diagnostic.code) + ": " + diagnostic.message);
-    } else {
-        mMessageStrings.push_back(diagnostic.message);
-    }
+    mMessageStrings.push_back(diagnostic.message);
 
     mMessages.push_back({nullptr, nullptr, tintSeverityToMessageType(diagnostic.severity), lineNum,
                          linePosInBytes, offsetInBytes, lengthInBytes, linePosInUTF16,
diff --git a/src/dawn/native/d3d/ShaderUtils.cpp b/src/dawn/native/d3d/ShaderUtils.cpp
index 6ec2f4a..7f85e73 100644
--- a/src/dawn/native/d3d/ShaderUtils.cpp
+++ b/src/dawn/native/d3d/ShaderUtils.cpp
@@ -254,7 +254,8 @@
 
     TRACE_EVENT0(tracePlatform.UnsafeGetValue(), General, "tint::hlsl::writer::Generate");
     auto result = tint::hlsl::writer::Generate(transformedProgram, options);
-    DAWN_INVALID_IF(!result, "An error occured while generating HLSL: %s", result.Failure());
+    DAWN_INVALID_IF(!result, "An error occurred while generating HLSL:\n%s",
+                    result.Failure().reason.str());
 
     compiledShader->usesVertexIndex = usesVertexIndex;
     compiledShader->usesInstanceIndex = usesInstanceIndex;
diff --git a/src/dawn/native/metal/ShaderModuleMTL.mm b/src/dawn/native/metal/ShaderModuleMTL.mm
index 5f1bd48..f263346 100644
--- a/src/dawn/native/metal/ShaderModuleMTL.mm
+++ b/src/dawn/native/metal/ShaderModuleMTL.mm
@@ -281,8 +281,8 @@
 
             TRACE_EVENT0(r.platform.UnsafeGetValue(), General, "tint::msl::writer::Generate");
             auto result = tint::msl::writer::Generate(program, options);
-            DAWN_INVALID_IF(!result, "An error occured while generating MSL: %s.",
-                            result.Failure());
+            DAWN_INVALID_IF(!result, "An error occurred while generating MSL:\n%s",
+                            result.Failure().reason.str());
 
             // Metal uses Clang to compile the shader as C++14. Disable everything in the -Wall
             // category. -Wunused-variable in particular comes up a lot in generated code, and some
diff --git a/src/dawn/native/opengl/ShaderModuleGL.cpp b/src/dawn/native/opengl/ShaderModuleGL.cpp
index 7f583a4..ed76dfa 100644
--- a/src/dawn/native/opengl/ShaderModuleGL.cpp
+++ b/src/dawn/native/opengl/ShaderModuleGL.cpp
@@ -299,8 +299,8 @@
             tintOptions.texture_builtins_from_uniform = r.textureBuiltinsFromUniform;
 
             auto result = tint::glsl::writer::Generate(program, tintOptions, r.entryPointName);
-            DAWN_INVALID_IF(!result, "An error occured while generating GLSL: %s.",
-                            result.Failure());
+            DAWN_INVALID_IF(!result, "An error occurred while generating GLSL:\n%s",
+                            result.Failure().reason.str());
 
             return GLSLCompilation{{std::move(result->glsl), needsPlaceholderSampler,
                                     result->needs_internal_uniform_buffer,
diff --git a/src/dawn/native/vulkan/ShaderModuleVk.cpp b/src/dawn/native/vulkan/ShaderModuleVk.cpp
index 533cfe7..79df5a8 100644
--- a/src/dawn/native/vulkan/ShaderModuleVk.cpp
+++ b/src/dawn/native/vulkan/ShaderModuleVk.cpp
@@ -359,8 +359,8 @@
 
             TRACE_EVENT0(r.platform.UnsafeGetValue(), General, "tint::spirv::writer::Generate()");
             auto tintResult = tint::spirv::writer::Generate(program, options);
-            DAWN_INVALID_IF(!tintResult, "An error occured while generating SPIR-V: %s.",
-                            tintResult.Failure());
+            DAWN_INVALID_IF(!tintResult, "An error occurred while generating SPIR-V\n%s",
+                            tintResult.Failure().reason.str());
 
             CompiledSpirv result;
             result.spirv = std::move(tintResult.Get().spirv);
diff --git a/src/dawn/tests/mocks/platform/CachingInterfaceMock.h b/src/dawn/tests/mocks/platform/CachingInterfaceMock.h
index d156632..82caba8 100644
--- a/src/dawn/tests/mocks/platform/CachingInterfaceMock.h
+++ b/src/dawn/tests/mocks/platform/CachingInterfaceMock.h
@@ -34,7 +34,7 @@
         EXPECT_EQ(N, after - before);         \
     } while (0)
 
-// Check that |HitN| cache hits occured, and |AddN| entries were added.
+// Check that |HitN| cache hits occurred, and |AddN| entries were added.
 // Usage: EXPECT_CACHE_STATS(myMockCache, Hit(42), Add(3), ...)
 // Hit / Add help readability, and enforce the args are passed correctly in the expected order.
 #define EXPECT_CACHE_STATS(cache, HitN, AddN, statement)                    \
diff --git a/src/dawn/tests/unittests/native/CommandBufferEncodingTests.cpp b/src/dawn/tests/unittests/native/CommandBufferEncodingTests.cpp
index e86ce6f..a752922 100644
--- a/src/dawn/tests/unittests/native/CommandBufferEncodingTests.cpp
+++ b/src/dawn/tests/unittests/native/CommandBufferEncodingTests.cpp
@@ -274,7 +274,7 @@
 }
 
 // Test that after restoring state, it is fully applied to the state tracker
-// and does not leak state changes that occured between a snapshot and the
+// and does not leak state changes that occurred between a snapshot and the
 // state restoration.
 TEST_F(CommandBufferEncodingTests, StateNotLeakedAfterRestore) {
     wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
diff --git a/src/tint/api/tint.cc b/src/tint/api/tint.cc
index 3cb2ebb..4d840f2 100644
--- a/src/tint/api/tint.cc
+++ b/src/tint/api/tint.cc
@@ -58,7 +58,7 @@
     tint::Program::printer = [](const tint::Program& program) {
         auto result = wgsl::writer::Generate(program, {});
         if (!result) {
-            return "error: " + result.Failure();
+            return result.Failure().reason.str();
         }
         return result->wgsl;
     };
diff --git a/src/tint/cmd/bench/main_bench.cc b/src/tint/cmd/bench/main_bench.cc
index 3ee05d3..e04dc38 100644
--- a/src/tint/cmd/bench/main_bench.cc
+++ b/src/tint/cmd/bench/main_bench.cc
@@ -109,7 +109,7 @@
             }
             auto result = tint::wgsl::writer::Generate(program, {});
             if (!result) {
-                return Error{result.Failure()};
+                return Error{result.Failure().reason.str()};
             }
             return tint::Source::File(path, result->wgsl);
         }
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index c943041..1a48a8d 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -400,9 +400,9 @@
         options.ShowHelp(std::cout);
     };
 
-    auto result = options.Parse(std::cerr, arguments);
+    auto result = options.Parse(arguments);
     if (!result) {
-        std::cerr << std::endl;
+        std::cerr << result.Failure() << std::endl;
         show_usage();
         return false;
     }
diff --git a/src/tint/fuzzers/generate_spirv_corpus.py b/src/tint/fuzzers/generate_spirv_corpus.py
index c6089015..6a865e0 100644
--- a/src/tint/fuzzers/generate_spirv_corpus.py
+++ b/src/tint/fuzzers/generate_spirv_corpus.py
@@ -81,7 +81,7 @@
 
     if num_errors > max_tolerated_errors:
         print("Too many (" + str(num_errors) +
-              ") errors occured while generating the SPIR-V corpus.")
+              ") errors occurred while generating the SPIR-V corpus.")
         print(logged_errors)
         return 1
 
diff --git a/src/tint/lang/core/BUILD.bazel b/src/tint/lang/core/BUILD.bazel
index be870fb..8b4f3e9 100644
--- a/src/tint/lang/core/BUILD.bazel
+++ b/src/tint/lang/core/BUILD.bazel
@@ -59,10 +59,14 @@
     "unary_op.h",
   ],
   deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
+    "//src/tint/utils/math",
     "//src/tint/utils/memory",
     "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ],
diff --git a/src/tint/lang/core/BUILD.cmake b/src/tint/lang/core/BUILD.cmake
index 8c85ad5..ec43104 100644
--- a/src/tint/lang/core/BUILD.cmake
+++ b/src/tint/lang/core/BUILD.cmake
@@ -63,10 +63,14 @@
 )
 
 tint_target_add_dependencies(tint_lang_core lib
+  tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_macros
+  tint_utils_math
   tint_utils_memory
   tint_utils_result
+  tint_utils_rtti
   tint_utils_text
   tint_utils_traits
 )
diff --git a/src/tint/lang/core/BUILD.gn b/src/tint/lang/core/BUILD.gn
index 59a0588..78c9e73 100644
--- a/src/tint/lang/core/BUILD.gn
+++ b/src/tint/lang/core/BUILD.gn
@@ -62,10 +62,14 @@
     "unary_op.h",
   ]
   deps = [
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
     "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
     "${tint_src_dir}/utils/memory",
     "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
     "${tint_src_dir}/utils/text",
     "${tint_src_dir}/utils/traits",
   ]
diff --git a/src/tint/lang/core/constant/eval.cc b/src/tint/lang/core/constant/eval.cc
index 74cc427..111629e 100644
--- a/src/tint/lang/core/constant/eval.cc
+++ b/src/tint/lang/core/constant/eval.cc
@@ -486,7 +486,6 @@
     for (uint32_t i = 0; i < n; i++) {
         if (auto el = TransformElements(mgr, composite_el_ty, f, index + i, cs->Index(i)...)) {
             els.Push(el.Get());
-
         } else {
             return el.Failure();
         }
@@ -515,7 +514,6 @@
     for (uint32_t i = 0; i < n; i++) {
         if (auto el = TransformUnaryElements(mgr, composite_el_ty, f, c0->Index(i))) {
             els.Push(el.Get());
-
         } else {
             return el.Failure();
         }
@@ -548,7 +546,6 @@
         if (auto el =
                 TransformBinaryElements(mgr, composite_el_ty, f, c0->Index(i), c1->Index(i))) {
             els.Push(el.Get());
-
         } else {
             return el.Failure();
         }
@@ -616,7 +613,6 @@
         if (auto el = TransformTernaryElements(mgr, composite_el_ty, f, c0->Index(i), c1->Index(i),
                                                c2->Index(i))) {
             els.Push(el.Get());
-
         } else {
             return el.Failure();
         }
@@ -639,7 +635,7 @@
             if (use_runtime_semantics_) {
                 return mgr.Zero(t);
             } else {
-                return tint::Failure;
+                return error;
             }
         }
     }
@@ -647,7 +643,7 @@
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Add(const Source& source, NumberT a, NumberT b) {
+tint::Result<NumberT, Eval::Error> Eval::Add(const Source& source, NumberT a, NumberT b) {
     NumberT result;
     if constexpr (IsAbstract<NumberT> || IsFloatingPoint<NumberT>) {
         if (auto r = CheckedAdd(a, b)) {
@@ -657,7 +653,7 @@
             if (use_runtime_semantics_) {
                 return NumberT{0};
             } else {
-                return tint::Failure;
+                return error;
             }
         }
     } else {
@@ -677,7 +673,7 @@
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Sub(const Source& source, NumberT a, NumberT b) {
+tint::Result<NumberT, Eval::Error> Eval::Sub(const Source& source, NumberT a, NumberT b) {
     NumberT result;
     if constexpr (IsAbstract<NumberT> || IsFloatingPoint<NumberT>) {
         if (auto r = CheckedSub(a, b)) {
@@ -687,7 +683,7 @@
             if (use_runtime_semantics_) {
                 return NumberT{0};
             } else {
-                return tint::Failure;
+                return error;
             }
         }
     } else {
@@ -707,7 +703,7 @@
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Mul(const Source& source, NumberT a, NumberT b) {
+tint::Result<NumberT, Eval::Error> Eval::Mul(const Source& source, NumberT a, NumberT b) {
     using T = UnwrapNumber<NumberT>;
     NumberT result;
     if constexpr (IsAbstract<NumberT> || IsFloatingPoint<NumberT>) {
@@ -718,7 +714,7 @@
             if (use_runtime_semantics_) {
                 return NumberT{0};
             } else {
-                return tint::Failure;
+                return error;
             }
         }
     } else {
@@ -737,7 +733,7 @@
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Div(const Source& source, NumberT a, NumberT b) {
+tint::Result<NumberT, Eval::Error> Eval::Div(const Source& source, NumberT a, NumberT b) {
     NumberT result;
     if constexpr (IsAbstract<NumberT> || IsFloatingPoint<NumberT>) {
         if (auto r = CheckedDiv(a, b)) {
@@ -747,7 +743,7 @@
             if (use_runtime_semantics_) {
                 return a;
             } else {
-                return tint::Failure;
+                return error;
             }
         }
     } else {
@@ -760,7 +756,7 @@
             if (use_runtime_semantics_) {
                 return a;
             } else {
-                return tint::Failure;
+                return error;
             }
         }
         if constexpr (std::is_signed_v<T>) {
@@ -771,7 +767,7 @@
                 if (use_runtime_semantics_) {
                     return a;
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
         }
@@ -781,7 +777,7 @@
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Mod(const Source& source, NumberT a, NumberT b) {
+tint::Result<NumberT, Eval::Error> Eval::Mod(const Source& source, NumberT a, NumberT b) {
     NumberT result;
     if constexpr (IsAbstract<NumberT> || IsFloatingPoint<NumberT>) {
         if (auto r = CheckedMod(a, b)) {
@@ -791,7 +787,7 @@
             if (use_runtime_semantics_) {
                 return NumberT{0};
             } else {
-                return tint::Failure;
+                return error;
             }
         }
     } else {
@@ -804,7 +800,7 @@
             if (use_runtime_semantics_) {
                 return NumberT{0};
             } else {
-                return tint::Failure;
+                return error;
             }
         }
         if constexpr (std::is_signed_v<T>) {
@@ -815,7 +811,7 @@
                 if (use_runtime_semantics_) {
                     return NumberT{0};
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
         }
@@ -825,100 +821,104 @@
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Dot2(const Source& source,
-                                 NumberT a1,
-                                 NumberT a2,
-                                 NumberT b1,
-                                 NumberT b2) {
+tint::Result<NumberT, Eval::Error> Eval::Dot2(const Source& source,
+                                              NumberT a1,
+                                              NumberT a2,
+                                              NumberT b1,
+                                              NumberT b2) {
     auto r1 = Mul(source, a1, b1);
     if (!r1) {
-        return tint::Failure;
+        return error;
     }
     auto r2 = Mul(source, a2, b2);
     if (!r2) {
-        return tint::Failure;
+        return error;
     }
     auto r = Add(source, r1.Get(), r2.Get());
     if (!r) {
-        return tint::Failure;
+        return error;
     }
     return r;
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Dot3(const Source& source,
-                                 NumberT a1,
-                                 NumberT a2,
-                                 NumberT a3,
-                                 NumberT b1,
-                                 NumberT b2,
-                                 NumberT b3) {
+tint::Result<NumberT, Eval::Error> Eval::Dot3(const Source& source,
+                                              NumberT a1,
+                                              NumberT a2,
+                                              NumberT a3,
+                                              NumberT b1,
+                                              NumberT b2,
+                                              NumberT b3) {
     auto r1 = Mul(source, a1, b1);
     if (!r1) {
-        return tint::Failure;
+        return error;
     }
     auto r2 = Mul(source, a2, b2);
     if (!r2) {
-        return tint::Failure;
+        return error;
     }
     auto r3 = Mul(source, a3, b3);
     if (!r3) {
-        return tint::Failure;
+        return error;
     }
     auto r = Add(source, r1.Get(), r2.Get());
     if (!r) {
-        return tint::Failure;
+        return error;
     }
     r = Add(source, r.Get(), r3.Get());
     if (!r) {
-        return tint::Failure;
+        return error;
     }
     return r;
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Dot4(const Source& source,
-                                 NumberT a1,
-                                 NumberT a2,
-                                 NumberT a3,
-                                 NumberT a4,
-                                 NumberT b1,
-                                 NumberT b2,
-                                 NumberT b3,
-                                 NumberT b4) {
+tint::Result<NumberT, Eval::Error> Eval::Dot4(const Source& source,
+                                              NumberT a1,
+                                              NumberT a2,
+                                              NumberT a3,
+                                              NumberT a4,
+                                              NumberT b1,
+                                              NumberT b2,
+                                              NumberT b3,
+                                              NumberT b4) {
     auto r1 = Mul(source, a1, b1);
     if (!r1) {
-        return tint::Failure;
+        return error;
     }
     auto r2 = Mul(source, a2, b2);
     if (!r2) {
-        return tint::Failure;
+        return error;
     }
     auto r3 = Mul(source, a3, b3);
     if (!r3) {
-        return tint::Failure;
+        return error;
     }
     auto r4 = Mul(source, a4, b4);
     if (!r4) {
-        return tint::Failure;
+        return error;
     }
     auto r = Add(source, r1.Get(), r2.Get());
     if (!r) {
-        return tint::Failure;
+        return error;
     }
     r = Add(source, r.Get(), r3.Get());
     if (!r) {
-        return tint::Failure;
+        return error;
     }
     r = Add(source, r.Get(), r4.Get());
     if (!r) {
-        return tint::Failure;
+        return error;
     }
     return r;
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Det2(const Source& source, NumberT a, NumberT b, NumberT c, NumberT d) {
+tint::Result<NumberT, Eval::Error> Eval::Det2(const Source& source,
+                                              NumberT a,
+                                              NumberT b,
+                                              NumberT c,
+                                              NumberT d) {
     // | a c |
     // | b d |
     //
@@ -928,30 +928,30 @@
 
     auto r1 = Mul(source, a, d);
     if (!r1) {
-        return tint::Failure;
+        return error;
     }
     auto r2 = Mul(source, c, b);
     if (!r2) {
-        return tint::Failure;
+        return error;
     }
     auto r = Sub(source, r1.Get(), r2.Get());
     if (!r) {
-        return tint::Failure;
+        return error;
     }
     return r;
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Det3(const Source& source,
-                                 NumberT a,
-                                 NumberT b,
-                                 NumberT c,
-                                 NumberT d,
-                                 NumberT e,
-                                 NumberT f,
-                                 NumberT g,
-                                 NumberT h,
-                                 NumberT i) {
+tint::Result<NumberT, Eval::Error> Eval::Det3(const Source& source,
+                                              NumberT a,
+                                              NumberT b,
+                                              NumberT c,
+                                              NumberT d,
+                                              NumberT e,
+                                              NumberT f,
+                                              NumberT g,
+                                              NumberT h,
+                                              NumberT i) {
     // | a d g |
     // | b e h |
     // | c f i |
@@ -963,53 +963,53 @@
 
     auto det1 = Det2(source, e, f, h, i);
     if (!det1) {
-        return tint::Failure;
+        return error;
     }
     auto a_det1 = Mul(source, a, det1.Get());
     if (!a_det1) {
-        return tint::Failure;
+        return error;
     }
     auto det2 = Det2(source, b, c, h, i);
     if (!det2) {
-        return tint::Failure;
+        return error;
     }
     auto d_det2 = Mul(source, d, det2.Get());
     if (!d_det2) {
-        return tint::Failure;
+        return error;
     }
     auto det3 = Det2(source, b, c, e, f);
     if (!det3) {
-        return tint::Failure;
+        return error;
     }
     auto g_det3 = Mul(source, g, det3.Get());
     if (!g_det3) {
-        return tint::Failure;
+        return error;
     }
     auto r = Sub(source, a_det1.Get(), d_det2.Get());
     if (!r) {
-        return tint::Failure;
+        return error;
     }
     return Add(source, r.Get(), g_det3.Get());
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Det4(const Source& source,
-                                 NumberT a,
-                                 NumberT b,
-                                 NumberT c,
-                                 NumberT d,
-                                 NumberT e,
-                                 NumberT f,
-                                 NumberT g,
-                                 NumberT h,
-                                 NumberT i,
-                                 NumberT j,
-                                 NumberT k,
-                                 NumberT l,
-                                 NumberT m,
-                                 NumberT n,
-                                 NumberT o,
-                                 NumberT p) {
+tint::Result<NumberT, Eval::Error> Eval::Det4(const Source& source,
+                                              NumberT a,
+                                              NumberT b,
+                                              NumberT c,
+                                              NumberT d,
+                                              NumberT e,
+                                              NumberT f,
+                                              NumberT g,
+                                              NumberT h,
+                                              NumberT i,
+                                              NumberT j,
+                                              NumberT k,
+                                              NumberT l,
+                                              NumberT m,
+                                              NumberT n,
+                                              NumberT o,
+                                              NumberT p) {
     // | a e i m |
     // | b f j n |
     // | c g k o |
@@ -1023,55 +1023,55 @@
 
     auto det1 = Det3(source, f, g, h, j, k, l, n, o, p);
     if (!det1) {
-        return tint::Failure;
+        return error;
     }
     auto a_det1 = Mul(source, a, det1.Get());
     if (!a_det1) {
-        return tint::Failure;
+        return error;
     }
     auto det2 = Det3(source, b, c, d, j, k, l, n, o, p);
     if (!det2) {
-        return tint::Failure;
+        return error;
     }
     auto e_det2 = Mul(source, e, det2.Get());
     if (!e_det2) {
-        return tint::Failure;
+        return error;
     }
     auto det3 = Det3(source, b, c, d, f, g, h, n, o, p);
     if (!det3) {
-        return tint::Failure;
+        return error;
     }
     auto i_det3 = Mul(source, i, det3.Get());
     if (!i_det3) {
-        return tint::Failure;
+        return error;
     }
     auto det4 = Det3(source, b, c, d, f, g, h, j, k, l);
     if (!det4) {
-        return tint::Failure;
+        return error;
     }
     auto m_det4 = Mul(source, m, det4.Get());
     if (!m_det4) {
-        return tint::Failure;
+        return error;
     }
     auto r = Sub(source, a_det1.Get(), e_det2.Get());
     if (!r) {
-        return tint::Failure;
+        return error;
     }
     r = Add(source, r.Get(), i_det3.Get());
     if (!r) {
-        return tint::Failure;
+        return error;
     }
     return Sub(source, r.Get(), m_det4.Get());
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Sqrt(const Source& source, NumberT v) {
+tint::Result<NumberT, Eval::Error> Eval::Sqrt(const Source& source, NumberT v) {
     if (v < NumberT(0)) {
         AddError("sqrt must be called with a value >= 0", source);
         if (use_runtime_semantics_) {
             return NumberT{0};
         } else {
-            return tint::Failure;
+            return error;
         }
     }
     return NumberT{std::sqrt(v)};
@@ -1082,18 +1082,21 @@
         if (auto r = Sqrt(source, v)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
 template <typename NumberT>
-tint::Result<NumberT> Eval::Clamp(const Source& source, NumberT e, NumberT low, NumberT high) {
+tint::Result<NumberT, Eval::Error> Eval::Clamp(const Source& source,
+                                               NumberT e,
+                                               NumberT low,
+                                               NumberT high) {
     if (low > high) {
         StringStream ss;
         ss << "clamp called with 'low' (" << low << ") greater than 'high' (" << high << ")";
         AddError(ss.str(), source);
         if (!use_runtime_semantics_) {
-            return tint::Failure;
+            return error;
         }
     }
     return NumberT{std::min(std::max(e, low), high)};
@@ -1104,7 +1107,7 @@
         if (auto r = Clamp(source, e, low, high)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1113,7 +1116,7 @@
         if (auto r = Add(source, a1, a2)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1122,7 +1125,7 @@
         if (auto r = Sub(source, a1, a2)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1131,7 +1134,7 @@
         if (auto r = Mul(source, a1, a2)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1140,7 +1143,7 @@
         if (auto r = Div(source, a1, a2)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1149,7 +1152,7 @@
         if (auto r = Mod(source, a1, a2)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1158,7 +1161,7 @@
         if (auto r = Dot2(source, a1, a2, b1, b2)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1167,7 +1170,7 @@
         if (auto r = Dot3(source, a1, a2, a3, b1, b2, b3)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1177,7 +1180,7 @@
         if (auto r = Dot4(source, a1, a2, a3, a4, b1, b2, b3, b4)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1203,7 +1206,7 @@
                 v2->Index(0), v2->Index(1), v2->Index(2), v2->Index(3));
     }
     TINT_ICE() << "Expected vector";
-    return Failure;
+    return error;
 }
 
 Eval::Result Eval::Length(const Source& source, const core::type::Type* ty, const Value* c0) {
@@ -1220,7 +1223,7 @@
     // Evaluates to sqrt(e[0]^2 + e[1]^2 + ...) if T is a vector type.
     auto d = Dot(source, c0, c0);
     if (!d) {
-        return tint::Failure;
+        return error;
     }
     return Dispatch_fa_f32_f16(SqrtFunc(source, ty), d.Get());
 }
@@ -1250,7 +1253,7 @@
         if (auto r = Det2(source, a, b, c, d)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1260,7 +1263,7 @@
         if (auto r = Det3(source, a, b, c, d, e, f, g, h, i)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1270,7 +1273,7 @@
         if (auto r = Det4(source, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p)) {
             return CreateScalar(source, elem_ty, r.Get());
         }
-        return tint::Failure;
+        return error;
     };
 }
 
@@ -1391,7 +1394,7 @@
         if (use_runtime_semantics_) {
             return mgr.Zero(el.type);
         } else {
-            return tint::Failure;
+            return error;
         }
     }
 
@@ -1471,7 +1474,7 @@
     Vector<const Value*, 4> els;
 
     // Reinterprets the buffer bits as destination element and push the result into the vector.
-    // Return false if an error occured, otherwise return true.
+    // Return false if an error occurred, otherwise return true.
     auto push_dst_element = [&](size_t offset) -> bool {
         uint32_t v;
         if (dst_el_ty->Size() == 4) {
@@ -1519,7 +1522,7 @@
     TINT_ASSERT((buffer.Length() == total_bitwidth));
     for (size_t i = 0; i < dst_count; i++) {
         if (!push_dst_element(i * dst_el_ty->Size())) {
-            return tint::Failure;
+            return error;
         }
     }
 
@@ -1642,7 +1645,7 @@
     for (size_t i = 0; i < mat_ty->rows(); ++i) {
         auto r = dot(args[0], i, args[1]);  // matrix row i * vector
         if (!r) {
-            return tint::Failure;
+            return error;
         }
         result.Push(r.Get());
     }
@@ -1692,7 +1695,7 @@
     for (size_t i = 0; i < mat_ty->columns(); ++i) {
         auto r = dot(args[0], args[1], i);  // vector * matrix col i
         if (!r) {
-            return tint::Failure;
+            return error;
         }
         result.Push(r.Get());
     }
@@ -1751,7 +1754,7 @@
         for (size_t r = 0; r < mat1_ty->rows(); ++r) {
             auto v = dot(mat1, r, mat2, c);  // mat1 row r * mat2 col c
             if (!v) {
-                return tint::Failure;
+                return error;
             }
             col_vec.Push(v.Get());  // mat1 row r * mat2 col c
         }
@@ -1955,7 +1958,7 @@
                     if ((e1u & mask) != 0 && (e1u & mask) != mask) {
                         AddError("shift left operation results in sign change", source);
                         if (!use_runtime_semantics_) {
-                            return tint::Failure;
+                            return error;
                         }
                     }
                 } else {
@@ -1963,7 +1966,7 @@
                     if (e1 != 0) {
                         AddError(OverflowErrorMessage(e1, "<<", e2), source);
                         if (!use_runtime_semantics_) {
-                            return tint::Failure;
+                            return error;
                         }
                     }
 
@@ -1983,7 +1986,7 @@
                     if (use_runtime_semantics_) {
                         e2u = e2u % bit_width;
                     } else {
-                        return tint::Failure;
+                        return error;
                     }
                 }
 
@@ -1995,7 +1998,7 @@
                     if ((e1u & mask) != 0 && (e1u & mask) != mask) {
                         AddError("shift left operation results in sign change", source);
                         if (!use_runtime_semantics_) {
-                            return tint::Failure;
+                            return error;
                         }
                     }
                 } else {
@@ -2007,7 +2010,7 @@
                         if ((e1u & mask) != 0) {
                             AddError(OverflowErrorMessage(e1, "<<", e2), source);
                             if (!use_runtime_semantics_) {
-                                return tint::Failure;
+                                return error;
                             }
                         }
                     }
@@ -2023,7 +2026,7 @@
 
     if (TINT_UNLIKELY(!args[1]->Type()->DeepestElement()->Is<core::type::U32>())) {
         TINT_ICE() << "Element type of rhs of ShiftLeft must be a u32";
-        return Failure;
+        return error;
     }
 
     return TransformBinaryElements(mgr, ty, transform, args[0], args[1]);
@@ -2073,7 +2076,7 @@
                     if (use_runtime_semantics_) {
                         e2u = e2u % bit_width;
                     } else {
-                        return tint::Failure;
+                        return error;
                     }
                 }
 
@@ -2090,7 +2093,7 @@
 
     if (TINT_UNLIKELY(!args[1]->Type()->DeepestElement()->Is<core::type::U32>())) {
         TINT_ICE() << "Element type of rhs of ShiftLeft must be a u32";
-        return Failure;
+        return error;
     }
 
     return TransformBinaryElements(mgr, ty, transform, args[0], args[1]);
@@ -2133,7 +2136,7 @@
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
             return CreateScalar(source, c0->Type(), NumberT(std::acos(i.value)));
@@ -2154,7 +2157,7 @@
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
             return CreateScalar(source, c0->Type(), NumberT(std::acosh(i.value)));
@@ -2189,7 +2192,7 @@
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
             return CreateScalar(source, c0->Type(), NumberT(std::asin(i.value)));
@@ -2236,7 +2239,7 @@
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
             return CreateScalar(source, c0->Type(), NumberT(std::atanh(i.value)));
@@ -2388,15 +2391,15 @@
 
     auto x = Dispatch_fa_f32_f16(Det2Func(source, elem_ty), u1, u2, v1, v2);
     if (!x) {
-        return tint::Failure;
+        return error;
     }
     auto y = Dispatch_fa_f32_f16(Det2Func(source, elem_ty), v0, v2, u0, u2);
     if (!y) {
-        return tint::Failure;
+        return error;
     }
     auto z = Dispatch_fa_f32_f16(Det2Func(source, elem_ty), u0, u1, v0, v1);
     if (!z) {
-        return tint::Failure;
+        return error;
     }
 
     return mgr.Composite(ty, Vector<const Value*, 3>{x.Get(), y.Get(), z.Get()});
@@ -2414,12 +2417,12 @@
             auto scale = Div(source, NumberT(180), NumberT(pi));
             if (!scale) {
                 AddNote("when calculating degrees", source);
-                return tint::Failure;
+                return error;
             }
             auto result = Mul(source, e, scale.Get());
             if (!result) {
                 AddNote("when calculating degrees", source);
-                return tint::Failure;
+                return error;
             }
             return CreateScalar(source, c0->Type(), result.Get());
         };
@@ -2455,7 +2458,7 @@
                                            me(0, 3), me(1, 3), me(2, 3), me(3, 3));
         }
         TINT_ICE() << "Unexpected number of matrix rows";
-        return Failure;
+        return error;
     };
     auto r = calculate();
     if (!r) {
@@ -2469,7 +2472,7 @@
                             const Source& source) {
     auto err = [&]() -> Eval::Result {
         AddNote("when calculating distance", source);
-        return tint::Failure;
+        return error;
     };
 
     auto minus = Minus(args[0]->Type(), args, source);
@@ -2506,7 +2509,7 @@
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
             return CreateScalar(source, c0->Type(), val);
@@ -2528,7 +2531,7 @@
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
             return CreateScalar(source, c0->Type(), val);
@@ -2565,7 +2568,7 @@
                     o = std::min(o, w);
                     c = std::min(c, w - o);
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
 
@@ -2608,7 +2611,7 @@
     auto r = Dot(source, e2, e3);
     if (!r) {
         AddNote("when calculating faceForward", source);
-        return tint::Failure;
+        return error;
     }
     auto is_negative = [](auto v) { return v < 0; };
     if (Dispatch_fa_f32_f16(is_negative, r.Get())) {
@@ -2706,7 +2709,7 @@
         auto create = [&](auto e1, auto e2, auto e3) -> Eval::Result {
             auto err_msg = [&] {
                 AddNote("when calculating fma", source);
-                return tint::Failure;
+                return error;
             };
 
             auto mul = Mul(source, e1, e2);
@@ -2775,7 +2778,7 @@
             [&](Default) {
                 TINT_ICE() << "unhandled element type for frexp() const-eval: "
                            << s->Type()->FriendlyName();
-                return FractExp{Failure, Failure};
+                return FractExp{error, error};
             });
     };
 
@@ -2785,7 +2788,7 @@
         for (uint32_t i = 0; i < vec->Width(); i++) {
             auto fe = scalar(arg->Index(i));
             if (!fe.fract || !fe.exp) {
-                return tint::Failure;
+                return error;
             }
             fract_els.Push(fe.fract.Get());
             exp_els.Push(fe.exp.Get());
@@ -2799,7 +2802,7 @@
     } else {
         auto fe = scalar(arg);
         if (!fe.fract || !fe.exp) {
-            return tint::Failure;
+            return error;
         }
         return mgr.Composite(ty, Vector<const Value*, 2>{
                                      fe.fract.Get(),
@@ -2836,7 +2839,7 @@
                     o = std::min(o, w);
                     c = std::min(c, w - o);
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
 
@@ -2877,13 +2880,13 @@
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
 
             auto err = [&] {
                 AddNote("when calculating inverseSqrt", source);
-                return tint::Failure;
+                return error;
             };
 
             auto s = Sqrt(source, e);
@@ -2934,7 +2937,7 @@
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c1->Type());
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
 
@@ -2970,7 +2973,7 @@
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
             return CreateScalar(source, c0->Type(), NumberT(std::log(v)));
@@ -2991,7 +2994,7 @@
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
             return CreateScalar(source, c0->Type(), NumberT(std::log2(v)));
@@ -3043,19 +3046,19 @@
             // float precision loss when e1 and e2 significantly differ in magnitude.
             auto one_sub_e3 = Sub(source, NumberT{1}, e3);
             if (!one_sub_e3) {
-                return tint::Failure;
+                return error;
             }
             auto e1_mul_one_sub_e3 = Mul(source, e1, one_sub_e3.Get());
             if (!e1_mul_one_sub_e3) {
-                return tint::Failure;
+                return error;
             }
             auto e2_mul_e3 = Mul(source, e2, e3);
             if (!e2_mul_e3) {
-                return tint::Failure;
+                return error;
             }
             auto r = Add(source, e1_mul_one_sub_e3.Get(), e2_mul_e3.Get());
             if (!r) {
-                return tint::Failure;
+                return error;
             }
             return CreateScalar(source, c0->Type(), r.Get());
         };
@@ -3089,13 +3092,13 @@
     if (auto fract = TransformUnaryElements(mgr, args[0]->Type(), transform_fract, args[0])) {
         fields.Push(fract.Get());
     } else {
-        return tint::Failure;
+        return error;
     }
 
     if (auto whole = TransformUnaryElements(mgr, args[0]->Type(), transform_whole, args[0])) {
         fields.Push(whole.Get());
     } else {
-        return tint::Failure;
+        return error;
     }
 
     return mgr.Composite(ty, std::move(fields));
@@ -3108,7 +3111,7 @@
     auto len = Length(source, len_ty, args[0]);
     if (!len) {
         AddNote("when calculating normalize", source);
-        return tint::Failure;
+        return error;
     }
     auto* v = len.Get();
     if (v->AllZero()) {
@@ -3116,7 +3119,7 @@
         if (use_runtime_semantics_) {
             return mgr.Zero(ty);
         } else {
-            return tint::Failure;
+            return error;
         }
     }
     return Divide(ty, Vector{args[0], v}, source);
@@ -3125,29 +3128,29 @@
 Eval::Result Eval::pack2x16float(const core::type::Type* ty,
                                  VectorRef<const Value*> args,
                                  const Source& source) {
-    auto convert = [&](f32 val) -> tint::Result<uint32_t> {
+    auto convert = [&](f32 val) -> tint::Result<uint32_t, Error> {
         auto conv = CheckedConvert<f16>(val);
         if (!conv) {
             AddError(OverflowErrorMessage(val, "f16"), source);
             if (use_runtime_semantics_) {
                 return 0;
             } else {
-                return tint::Failure;
+                return error;
             }
         }
         uint16_t v = conv.Get().BitsRepresentation();
-        return tint::Result<uint32_t>{v};
+        return tint::Result<uint32_t, Error>{v};
     };
 
     auto* e = args[0];
     auto e0 = convert(e->Index(0)->ValueAs<f32>());
     if (!e0) {
-        return tint::Failure;
+        return error;
     }
 
     auto e1 = convert(e->Index(1)->ValueAs<f32>());
     if (!e1) {
-        return tint::Failure;
+        return error;
     }
 
     u32 ret = u32((e0.Get() & 0x0000'ffff) | (e1.Get() << 16));
@@ -3237,7 +3240,7 @@
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
-                    return tint::Failure;
+                    return error;
                 }
             }
             return CreateScalar(source, c0->Type(), *r);
@@ -3259,12 +3262,12 @@
             auto scale = Div(source, NumberT(pi), NumberT(180));
             if (!scale) {
                 AddNote("when calculating radians", source);
-                return tint::Failure;
+                return error;
             }
             auto result = Mul(source, e, scale.Get());
             if (!result) {
                 AddNote("when calculating radians", source);
-                return tint::Failure;
+                return error;
             }
             return CreateScalar(source, c0->Type(), result.Get());
         };
@@ -3287,7 +3290,7 @@
         // dot(e2, e1)
         auto dot_e2_e1 = Dot(source, e2, e1);
         if (!dot_e2_e1) {
-            return tint::Failure;
+            return error;
         }
 
         // 2 * dot(e2, e1)
@@ -3297,13 +3300,13 @@
         };
         auto dot_e2_e1_2 = Dispatch_fa_f32_f16(mul2, dot_e2_e1.Get());
         if (!dot_e2_e1_2) {
-            return tint::Failure;
+            return error;
         }
 
         // 2 * dot(e2, e1) * e2
         auto dot_e2_e1_2_e2 = Mul(source, ty, dot_e2_e1_2.Get(), e2);
         if (!dot_e2_e1_2_e2) {
-            return tint::Failure;
+            return error;
         }
 
         // e1 - 2 * dot(e2, e1) * e2
@@ -3327,23 +3330,23 @@
         // let k = 1.0 - e3 * e3 * (1.0 - dot(e2, e1) * dot(e2, e1))
         auto e3_squared = Mul(source, e3, e3);
         if (!e3_squared) {
-            return tint::Failure;
+            return error;
         }
         auto dot_e2_e1_squared = Mul(source, dot_e2_e1, dot_e2_e1);
         if (!dot_e2_e1_squared) {
-            return tint::Failure;
+            return error;
         }
         auto r = Sub(source, NumberT(1), dot_e2_e1_squared.Get());
         if (!r) {
-            return tint::Failure;
+            return error;
         }
         r = Mul(source, e3_squared.Get(), r.Get());
         if (!r) {
-            return tint::Failure;
+            return error;
         }
         r = Sub(source, NumberT(1), r.Get());
         if (!r) {
-            return tint::Failure;
+            return error;
         }
         return CreateScalar(source, el_ty, r.Get());
     };
@@ -3352,15 +3355,15 @@
         // e3 * dot(e2, e1) + sqrt(k)
         auto sqrt_k = Sqrt(source, k);
         if (!sqrt_k) {
-            return tint::Failure;
+            return error;
         }
         auto r = Mul(source, e3, dot_e2_e1);
         if (!r) {
-            return tint::Failure;
+            return error;
         }
         r = Add(source, r.Get(), sqrt_k.Get());
         if (!r) {
-            return tint::Failure;
+            return error;
         }
         return CreateScalar(source, el_ty, r.Get());
     };
@@ -3378,13 +3381,13 @@
         // dot(e2, e1)
         auto dot_e2_e1 = Dot(source, e2, e1);
         if (!dot_e2_e1) {
-            return tint::Failure;
+            return error;
         }
 
         // let k = 1.0 - e3 * e3 * (1.0 - dot(e2, e1) * dot(e2, e1))
         auto k = Dispatch_fa_f32_f16(compute_k, e3, dot_e2_e1.Get());
         if (!k) {
-            return tint::Failure;
+            return error;
         }
 
         // If k < 0.0, returns the refraction vector 0.0
@@ -3395,15 +3398,15 @@
         // Otherwise return the refraction vector e3 * e1 - (e3 * dot(e2, e1) + sqrt(k)) * e2
         auto e1_scaled = Mul(source, ty, e3, e1);
         if (!e1_scaled) {
-            return tint::Failure;
+            return error;
         }
         auto e2_scale = Dispatch_fa_f32_f16(compute_e2_scale, e3, dot_e2_e1.Get(), k.Get());
         if (!e2_scale) {
-            return tint::Failure;
+            return error;
         }
         auto e2_scaled = Mul(source, ty, e2_scale.Get(), e2);
         if (!e2_scaled) {
-            return tint::Failure;
+            return error;
         }
         return Sub(source, ty, e1_scaled.Get(), e2_scaled.Get());
     };
@@ -3577,7 +3580,7 @@
 
             auto err = [&] {
                 AddNote("when calculating smoothstep", source);
-                return tint::Failure;
+                return error;
             };
 
             // t = clamp((x - low) / (high - low), 0.0, 1.0)
@@ -3716,7 +3719,7 @@
             if (use_runtime_semantics_) {
                 val = f32(0.f);
             } else {
-                return tint::Failure;
+                return error;
             }
         }
         auto el = CreateScalar(source, inner_ty, val.Get());
@@ -3817,7 +3820,7 @@
             if (use_runtime_semantics_) {
                 return mgr.Zero(c->Type());
             } else {
-                return tint::Failure;
+                return error;
             }
         }
         return CreateScalar(source, c->Type(), conv.Get());
@@ -3833,7 +3836,7 @@
     }
     ConvertContext ctx{mgr, diags, source, use_runtime_semantics_};
     auto* converted = ConvertInternal(value, target_ty, ctx);
-    return converted ? Result(converted) : tint::Failure;
+    return converted ? Result(converted) : Result(error);
 }
 
 void Eval::AddError(const std::string& msg, const Source& source) const {
diff --git a/src/tint/lang/core/constant/eval.h b/src/tint/lang/core/constant/eval.h
index 17f298e..3896d72 100644
--- a/src/tint/lang/core/constant/eval.h
+++ b/src/tint/lang/core/constant/eval.h
@@ -41,15 +41,23 @@
 /// Eval performs shader creation-time (const-expression) expression evaluation.
 class Eval {
   public:
+    /// The failure type used by the methods of this class.
+    /// TODO(bclayton): Use Failure, and bubble up the error diagnostics instead of writing directly
+    /// to the diagnostic list.
+    struct Error {};
+
+    /// A value of the type Error.
+    static constexpr Error error{};
+
     /// The result type of a method that may raise a diagnostic error, upon which the caller should
     /// handle the error. Can be one of three distinct values:
     /// * A non-null Value pointer. Returned when a expression resolves to a creation
     ///   time value.
     /// * A null Value pointer. Returned when a expression cannot resolve to a creation time value,
     ///   but is otherwise legal.
-    /// * `tint::Failure`. Returned when there was an error. In this situation the method will have
+    /// * Error. Returned when there was an error. In this situation the method will have
     ///   already reported a diagnostic error message, and the caller should abort resolving.
-    using Result = tint::Result<const Value*>;
+    using Result = tint::Result<const Value*, Error>;
 
     /// Typedef for a constant evaluation function
     using Function = Result (Eval::*)(const core::type::Type* result_ty,
@@ -959,7 +967,7 @@
     /// @param b the rhs number
     /// @returns the result number on success, or logs an error and returns Failure
     template <typename NumberT>
-    tint::Result<NumberT> Add(const Source& source, NumberT a, NumberT b);
+    tint::Result<NumberT, Error> Add(const Source& source, NumberT a, NumberT b);
 
     /// Subtracts two Number<T>s
     /// @param source the source location
@@ -967,7 +975,7 @@
     /// @param b the rhs number
     /// @returns the result number on success, or logs an error and returns Failure
     template <typename NumberT>
-    tint::Result<NumberT> Sub(const Source& source, NumberT a, NumberT b);
+    tint::Result<NumberT, Error> Sub(const Source& source, NumberT a, NumberT b);
 
     /// Multiplies two Number<T>s
     /// @param source the source location
@@ -975,7 +983,7 @@
     /// @param b the rhs number
     /// @returns the result number on success, or logs an error and returns Failure
     template <typename NumberT>
-    tint::Result<NumberT> Mul(const Source& source, NumberT a, NumberT b);
+    tint::Result<NumberT, Error> Mul(const Source& source, NumberT a, NumberT b);
 
     /// Divides two Number<T>s
     /// @param source the source location
@@ -983,7 +991,7 @@
     /// @param b the rhs number
     /// @returns the result number on success, or logs an error and returns Failure
     template <typename NumberT>
-    tint::Result<NumberT> Div(const Source& source, NumberT a, NumberT b);
+    tint::Result<NumberT, Error> Div(const Source& source, NumberT a, NumberT b);
 
     /// Returns the (signed) remainder of the division of two Number<T>s
     /// @param source the source location
@@ -991,7 +999,7 @@
     /// @param b the rhs number
     /// @returns the result number on success, or logs an error and returns Failure
     template <typename NumberT>
-    tint::Result<NumberT> Mod(const Source& source, NumberT a, NumberT b);
+    tint::Result<NumberT, Error> Mod(const Source& source, NumberT a, NumberT b);
 
     /// Returns the dot product of (a1,a2) with (b1,b2)
     /// @param source the source location
@@ -1001,11 +1009,11 @@
     /// @param b2 component 2 of rhs vector
     /// @returns the result number on success, or logs an error and returns Failure
     template <typename NumberT>
-    tint::Result<NumberT> Dot2(const Source& source,
-                               NumberT a1,
-                               NumberT a2,
-                               NumberT b1,
-                               NumberT b2);
+    tint::Result<NumberT, Error> Dot2(const Source& source,
+                                      NumberT a1,
+                                      NumberT a2,
+                                      NumberT b1,
+                                      NumberT b2);
 
     /// Returns the dot product of (a1,a2,a3) with (b1,b2,b3)
     /// @param source the source location
@@ -1017,13 +1025,13 @@
     /// @param b3 component 3 of rhs vector
     /// @returns the result number on success, or logs an error and returns Failure
     template <typename NumberT>
-    tint::Result<NumberT> Dot3(const Source& source,
-                               NumberT a1,
-                               NumberT a2,
-                               NumberT a3,
-                               NumberT b1,
-                               NumberT b2,
-                               NumberT b3);
+    tint::Result<NumberT, Error> Dot3(const Source& source,
+                                      NumberT a1,
+                                      NumberT a2,
+                                      NumberT a3,
+                                      NumberT b1,
+                                      NumberT b2,
+                                      NumberT b3);
 
     /// Returns the dot product of (a1,b1,c1,d1) with (a2,b2,c2,d2)
     /// @param source the source location
@@ -1037,15 +1045,15 @@
     /// @param b4 component 4 of rhs vector
     /// @returns the result number on success, or logs an error and returns Failure
     template <typename NumberT>
-    tint::Result<NumberT> Dot4(const Source& source,
-                               NumberT a1,
-                               NumberT a2,
-                               NumberT a3,
-                               NumberT a4,
-                               NumberT b1,
-                               NumberT b2,
-                               NumberT b3,
-                               NumberT b4);
+    tint::Result<NumberT, Error> Dot4(const Source& source,
+                                      NumberT a1,
+                                      NumberT a2,
+                                      NumberT a3,
+                                      NumberT a4,
+                                      NumberT b1,
+                                      NumberT b2,
+                                      NumberT b3,
+                                      NumberT b4);
 
     /// Returns the determinant of the 2x2 matrix:
     /// | a c |
@@ -1056,11 +1064,11 @@
     /// @param c component 1 of the second column vector
     /// @param d component 2 of the second column vector
     template <typename NumberT>
-    tint::Result<NumberT> Det2(const Source& source,  //
-                               NumberT a,
-                               NumberT b,
-                               NumberT c,
-                               NumberT d);
+    tint::Result<NumberT, Error> Det2(const Source& source,  //
+                                      NumberT a,
+                                      NumberT b,
+                                      NumberT c,
+                                      NumberT d);
 
     /// Returns the determinant of the 3x3 matrix:
     /// | a d g |
@@ -1077,16 +1085,16 @@
     /// @param h component 2 of the third column vector
     /// @param i component 3 of the third column vector
     template <typename NumberT>
-    tint::Result<NumberT> Det3(const Source& source,
-                               NumberT a,
-                               NumberT b,
-                               NumberT c,
-                               NumberT d,
-                               NumberT e,
-                               NumberT f,
-                               NumberT g,
-                               NumberT h,
-                               NumberT i);
+    tint::Result<NumberT, Error> Det3(const Source& source,
+                                      NumberT a,
+                                      NumberT b,
+                                      NumberT c,
+                                      NumberT d,
+                                      NumberT e,
+                                      NumberT f,
+                                      NumberT g,
+                                      NumberT h,
+                                      NumberT i);
 
     /// Returns the determinant of the 4x4 matrix:
     /// | a e i m |
@@ -1111,26 +1119,26 @@
     /// @param o component 3 of the fourth column vector
     /// @param p component 4 of the fourth column vector
     template <typename NumberT>
-    tint::Result<NumberT> Det4(const Source& source,
-                               NumberT a,
-                               NumberT b,
-                               NumberT c,
-                               NumberT d,
-                               NumberT e,
-                               NumberT f,
-                               NumberT g,
-                               NumberT h,
-                               NumberT i,
-                               NumberT j,
-                               NumberT k,
-                               NumberT l,
-                               NumberT m,
-                               NumberT n,
-                               NumberT o,
-                               NumberT p);
+    tint::Result<NumberT, Error> Det4(const Source& source,
+                                      NumberT a,
+                                      NumberT b,
+                                      NumberT c,
+                                      NumberT d,
+                                      NumberT e,
+                                      NumberT f,
+                                      NumberT g,
+                                      NumberT h,
+                                      NumberT i,
+                                      NumberT j,
+                                      NumberT k,
+                                      NumberT l,
+                                      NumberT m,
+                                      NumberT n,
+                                      NumberT o,
+                                      NumberT p);
 
     template <typename NumberT>
-    tint::Result<NumberT> Sqrt(const Source& source, NumberT v);
+    tint::Result<NumberT, Error> Sqrt(const Source& source, NumberT v);
 
     /// Clamps e between low and high
     /// @param source the source location
@@ -1139,7 +1147,7 @@
     /// @param high the upper bound
     /// @returns the result number on success, or logs an error and returns Failure
     template <typename NumberT>
-    tint::Result<NumberT> Clamp(const Source& source, NumberT e, NumberT low, NumberT high);
+    tint::Result<NumberT, Error> Clamp(const Source& source, NumberT e, NumberT low, NumberT high);
 
     /// Returns a callable that calls Add, and creates a Constant with its result of type `elem_ty`
     /// if successful, or returns Failure otherwise.
diff --git a/src/tint/lang/core/intrinsic/data/BUILD.bazel b/src/tint/lang/core/intrinsic/data/BUILD.bazel
index 314ffb3..7359e8e 100644
--- a/src/tint/lang/core/intrinsic/data/BUILD.bazel
+++ b/src/tint/lang/core/intrinsic/data/BUILD.bazel
@@ -38,6 +38,7 @@
     "//src/tint/lang/core/intrinsic",
     "//src/tint/lang/core/type",
     "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/id",
     "//src/tint/utils/macros",
diff --git a/src/tint/lang/core/intrinsic/data/BUILD.cmake b/src/tint/lang/core/intrinsic/data/BUILD.cmake
index 8134896..dc4cb63 100644
--- a/src/tint/lang/core/intrinsic/data/BUILD.cmake
+++ b/src/tint/lang/core/intrinsic/data/BUILD.cmake
@@ -37,6 +37,7 @@
   tint_lang_core_intrinsic
   tint_lang_core_type
   tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_id
   tint_utils_macros
diff --git a/src/tint/lang/core/intrinsic/data/BUILD.gn b/src/tint/lang/core/intrinsic/data/BUILD.gn
index 4a096a5..289deb7 100644
--- a/src/tint/lang/core/intrinsic/data/BUILD.gn
+++ b/src/tint/lang/core/intrinsic/data/BUILD.gn
@@ -37,6 +37,7 @@
     "${tint_src_dir}/lang/core/intrinsic",
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
     "${tint_src_dir}/utils/id",
     "${tint_src_dir}/utils/macros",
diff --git a/src/tint/lang/core/intrinsic/table.cc b/src/tint/lang/core/intrinsic/table.cc
index ce4c23e..564b5e0 100644
--- a/src/tint/lang/core/intrinsic/table.cc
+++ b/src/tint/lang/core/intrinsic/table.cc
@@ -220,7 +220,7 @@
         // Sort the candidates with the most promising first
         SortCandidates(candidates);
         on_no_match(std::move(candidates));
-        return Failure;
+        return Failure{};
     }
 
     Candidate match;
@@ -232,7 +232,7 @@
                                  std::move(templates));
         if (!match.overload) {
             // Ambiguous overload. ResolveCandidate() will have already raised an error diagnostic.
-            return Failure;
+            return Failure{};
         }
     }
 
@@ -246,7 +246,7 @@
                           .Type(&any);
         if (TINT_UNLIKELY(!return_type)) {
             TINT_ICE() << "MatchState.Match() returned null";
-            return Failure;
+            return Failure{};
         }
     } else {
         return_type = context.types.void_();
@@ -638,7 +638,7 @@
             break;
         default:
             TINT_UNREACHABLE() << "invalid unary op: " << op;
-            return Failure;
+            return Failure{};
     }
 
     Vector args{arg};
diff --git a/src/tint/lang/core/ir/transform/add_empty_entry_point.cc b/src/tint/lang/core/ir/transform/add_empty_entry_point.cc
index aef5ff8..1da621f 100644
--- a/src/tint/lang/core/ir/transform/add_empty_entry_point.cc
+++ b/src/tint/lang/core/ir/transform/add_empty_entry_point.cc
@@ -39,7 +39,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> AddEmptyEntryPoint(Module* ir) {
+Result<SuccessType> AddEmptyEntryPoint(Module* ir) {
     auto result = ValidateAndDumpIfNeeded(*ir, "AddEmptyEntryPoint transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/core/ir/transform/add_empty_entry_point.h b/src/tint/lang/core/ir/transform/add_empty_entry_point.h
index 2473d93..3f419bb 100644
--- a/src/tint/lang/core/ir/transform/add_empty_entry_point.h
+++ b/src/tint/lang/core/ir/transform/add_empty_entry_point.h
@@ -28,8 +28,8 @@
 
 /// Add an empty entry point to the module, if no other entry points exist.
 /// @param module the module to transform
-/// @returns an error string on failure
-Result<SuccessType, std::string> AddEmptyEntryPoint(Module* module);
+/// @returns success or failure
+Result<SuccessType> AddEmptyEntryPoint(Module* module);
 
 }  // namespace tint::core::ir::transform
 
diff --git a/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc b/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc
index 4acfbfb..b9be567 100644
--- a/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.cc
@@ -170,7 +170,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> Bgra8UnormPolyfill(Module* ir) {
+Result<SuccessType> Bgra8UnormPolyfill(Module* ir) {
     auto result = ValidateAndDumpIfNeeded(*ir, "Bgra8UnormPolyfill transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.h b/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.h
index 46c788f..2b67c52 100644
--- a/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.h
+++ b/src/tint/lang/core/ir/transform/bgra8unorm_polyfill.h
@@ -29,8 +29,8 @@
 /// Bgra8UnormPolyfill is a transform that changes the texel format of storage textures from
 /// bgra8unorm to rgba8unorm, inserting swizzles before and after texture accesses as necessary.
 /// @param module the module to transform
-/// @returns an error string on failure
-Result<SuccessType, std::string> Bgra8UnormPolyfill(Module* module);
+/// @returns success or failure
+Result<SuccessType> Bgra8UnormPolyfill(Module* module);
 
 }  // namespace tint::core::ir::transform
 
diff --git a/src/tint/lang/core/ir/transform/binary_polyfill.cc b/src/tint/lang/core/ir/transform/binary_polyfill.cc
index e4bed28..1988398 100644
--- a/src/tint/lang/core/ir/transform/binary_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/binary_polyfill.cc
@@ -232,7 +232,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> BinaryPolyfill(Module* ir, const BinaryPolyfillConfig& config) {
+Result<SuccessType> BinaryPolyfill(Module* ir, const BinaryPolyfillConfig& config) {
     auto result = ValidateAndDumpIfNeeded(*ir, "BinaryPolyfill transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/core/ir/transform/binary_polyfill.h b/src/tint/lang/core/ir/transform/binary_polyfill.h
index a01d6bb..ff41255 100644
--- a/src/tint/lang/core/ir/transform/binary_polyfill.h
+++ b/src/tint/lang/core/ir/transform/binary_polyfill.h
@@ -38,8 +38,8 @@
 /// backend dialects that may have different semantics.
 /// @param module the module to transform
 /// @param config the polyfill configuration
-/// @returns an error string on failure
-Result<SuccessType, std::string> BinaryPolyfill(Module* module, const BinaryPolyfillConfig& config);
+/// @returns success or failure
+Result<SuccessType> BinaryPolyfill(Module* module, const BinaryPolyfillConfig& config);
 
 }  // namespace tint::core::ir::transform
 
diff --git a/src/tint/lang/core/ir/transform/binding_remapper.cc b/src/tint/lang/core/ir/transform/binding_remapper.cc
index cbb023d..24118ee 100644
--- a/src/tint/lang/core/ir/transform/binding_remapper.cc
+++ b/src/tint/lang/core/ir/transform/binding_remapper.cc
@@ -28,9 +28,9 @@
 
 namespace {
 
-Result<SuccessType, std::string> Run(ir::Module* ir, const BindingRemapperOptions& options) {
+Result<SuccessType> Run(ir::Module* ir, const BindingRemapperOptions& options) {
     if (!options.access_controls.empty()) {
-        return std::string("remapping access controls is currently unsupported");
+        return Failure{"remapping access controls is currently unsupported"};
     }
     if (options.binding_points.empty()) {
         return Success;
@@ -63,8 +63,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> BindingRemapper(Module* ir,
-                                                 const BindingRemapperOptions& options) {
+Result<SuccessType> BindingRemapper(Module* ir, const BindingRemapperOptions& options) {
     auto result = ValidateAndDumpIfNeeded(*ir, "BindingRemapper transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/core/ir/transform/binding_remapper.h b/src/tint/lang/core/ir/transform/binding_remapper.h
index 821f1d9..232d2de 100644
--- a/src/tint/lang/core/ir/transform/binding_remapper.h
+++ b/src/tint/lang/core/ir/transform/binding_remapper.h
@@ -30,9 +30,8 @@
 /// BindingRemapper is a transform that remaps binding point indices and access controls.
 /// @param module the module to transform
 /// @param options the remapping options
-/// @returns an error string on failure
-Result<SuccessType, std::string> BindingRemapper(Module* module,
-                                                 const BindingRemapperOptions& options);
+/// @returns success or failure
+Result<SuccessType> BindingRemapper(Module* module, const BindingRemapperOptions& options);
 
 }  // namespace tint::core::ir::transform
 
diff --git a/src/tint/lang/core/ir/transform/block_decorated_structs.cc b/src/tint/lang/core/ir/transform/block_decorated_structs.cc
index 3fe380f..78125bc 100644
--- a/src/tint/lang/core/ir/transform/block_decorated_structs.cc
+++ b/src/tint/lang/core/ir/transform/block_decorated_structs.cc
@@ -111,7 +111,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> BlockDecoratedStructs(Module* ir) {
+Result<SuccessType> BlockDecoratedStructs(Module* ir) {
     auto result = ValidateAndDumpIfNeeded(*ir, "BlockDecoratedStructs transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/core/ir/transform/block_decorated_structs.h b/src/tint/lang/core/ir/transform/block_decorated_structs.h
index ae5e065..e4a2216 100644
--- a/src/tint/lang/core/ir/transform/block_decorated_structs.h
+++ b/src/tint/lang/core/ir/transform/block_decorated_structs.h
@@ -30,8 +30,8 @@
 /// structure that is recognized as needing a block decoration in SPIR-V, potentially wrapping the
 /// existing store type in a new structure if necessary.
 /// @param module the module to transform
-/// @returns an error string on failure
-Result<SuccessType, std::string> BlockDecoratedStructs(Module* module);
+/// @returns success or failure
+Result<SuccessType> BlockDecoratedStructs(Module* module);
 
 }  // namespace tint::core::ir::transform
 
diff --git a/src/tint/lang/core/ir/transform/builtin_polyfill.cc b/src/tint/lang/core/ir/transform/builtin_polyfill.cc
index e5d13d88..bf9626b 100644
--- a/src/tint/lang/core/ir/transform/builtin_polyfill.cc
+++ b/src/tint/lang/core/ir/transform/builtin_polyfill.cc
@@ -457,7 +457,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> BuiltinPolyfill(Module* ir, const BuiltinPolyfillConfig& config) {
+Result<SuccessType> BuiltinPolyfill(Module* ir, const BuiltinPolyfillConfig& config) {
     auto result = ValidateAndDumpIfNeeded(*ir, "BuiltinPolyfill transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/core/ir/transform/builtin_polyfill.h b/src/tint/lang/core/ir/transform/builtin_polyfill.h
index df537aa..5f52875 100644
--- a/src/tint/lang/core/ir/transform/builtin_polyfill.h
+++ b/src/tint/lang/core/ir/transform/builtin_polyfill.h
@@ -46,9 +46,8 @@
 /// features with equivalent alternatives.
 /// @param module the module to transform
 /// @param config the polyfill configuration
-/// @returns an error string on failure
-Result<SuccessType, std::string> BuiltinPolyfill(Module* module,
-                                                 const BuiltinPolyfillConfig& config);
+/// @returns success or failure
+Result<SuccessType> BuiltinPolyfill(Module* module, const BuiltinPolyfillConfig& config);
 
 }  // namespace tint::core::ir::transform
 
diff --git a/src/tint/lang/core/ir/transform/demote_to_helper.cc b/src/tint/lang/core/ir/transform/demote_to_helper.cc
index 0226e99..5a8bb39 100644
--- a/src/tint/lang/core/ir/transform/demote_to_helper.cc
+++ b/src/tint/lang/core/ir/transform/demote_to_helper.cc
@@ -203,7 +203,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> DemoteToHelper(Module* ir) {
+Result<SuccessType> DemoteToHelper(Module* ir) {
     auto result = ValidateAndDumpIfNeeded(*ir, "DemoteToHelper transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/core/ir/transform/demote_to_helper.h b/src/tint/lang/core/ir/transform/demote_to_helper.h
index b80ce75..8866962 100644
--- a/src/tint/lang/core/ir/transform/demote_to_helper.h
+++ b/src/tint/lang/core/ir/transform/demote_to_helper.h
@@ -33,8 +33,8 @@
 /// derivative operations. We do this by setting a global flag and masking all writes to storage
 /// buffers and textures.
 /// @param module the module to transform
-/// @returns an error string on failure
-Result<SuccessType, std::string> DemoteToHelper(Module* module);
+/// @returns success or failure
+Result<SuccessType> DemoteToHelper(Module* module);
 
 }  // namespace tint::core::ir::transform
 
diff --git a/src/tint/lang/core/ir/transform/helper_test.h b/src/tint/lang/core/ir/transform/helper_test.h
index 71b2c22..4363b94 100644
--- a/src/tint/lang/core/ir/transform/helper_test.h
+++ b/src/tint/lang/core/ir/transform/helper_test.h
@@ -45,7 +45,7 @@
 
         // Validate the output IR.
         auto valid = ir::Validate(mod);
-        EXPECT_TRUE(valid) << valid.Failure().str();
+        EXPECT_TRUE(valid) << valid.Failure().reason.str();
     }
 
     /// @returns the transformed module as a disassembled string
diff --git a/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc b/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
index 2a94be1..a62490d 100644
--- a/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
+++ b/src/tint/lang/core/ir/transform/multiplanar_external_texture.cc
@@ -568,8 +568,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> MultiplanarExternalTexture(Module* ir,
-                                                            const ExternalTextureOptions& options) {
+Result<SuccessType> MultiplanarExternalTexture(Module* ir, const ExternalTextureOptions& options) {
     auto result = ValidateAndDumpIfNeeded(*ir, "MultiplanarExternalTexture transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/core/ir/transform/multiplanar_external_texture.h b/src/tint/lang/core/ir/transform/multiplanar_external_texture.h
index 2fc6dd9..4837fcc 100644
--- a/src/tint/lang/core/ir/transform/multiplanar_external_texture.h
+++ b/src/tint/lang/core/ir/transform/multiplanar_external_texture.h
@@ -32,9 +32,9 @@
 /// parameters that describe how the texture should be sampled.
 /// @param module the module to transform
 /// @param options the external texture options
-/// @returns an error string on failure
-Result<SuccessType, std::string> MultiplanarExternalTexture(Module* module,
-                                                            const ExternalTextureOptions& options);
+/// @returns success or failure
+Result<SuccessType> MultiplanarExternalTexture(Module* module,
+                                               const ExternalTextureOptions& options);
 
 }  // namespace tint::core::ir::transform
 
diff --git a/src/tint/lang/core/ir/transform/robustness.cc b/src/tint/lang/core/ir/transform/robustness.cc
index 77216e1..72ea908 100644
--- a/src/tint/lang/core/ir/transform/robustness.cc
+++ b/src/tint/lang/core/ir/transform/robustness.cc
@@ -335,7 +335,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> Robustness(Module* ir, const RobustnessConfig& config) {
+Result<SuccessType> Robustness(Module* ir, const RobustnessConfig& config) {
     auto result = ValidateAndDumpIfNeeded(*ir, "Robustness transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/core/ir/transform/robustness.h b/src/tint/lang/core/ir/transform/robustness.h
index 82d0452..ebe4f91 100644
--- a/src/tint/lang/core/ir/transform/robustness.h
+++ b/src/tint/lang/core/ir/transform/robustness.h
@@ -59,8 +59,8 @@
 /// Robustness is a transform that prevents out-of-bounds memory accesses.
 /// @param module the module to transform
 /// @param config the robustness configuration
-/// @returns an error string on failure
-Result<SuccessType, std::string> Robustness(Module* module, const RobustnessConfig& config);
+/// @returns success or failure
+Result<SuccessType> Robustness(Module* module, const RobustnessConfig& config);
 
 }  // namespace tint::core::ir::transform
 
diff --git a/src/tint/lang/core/ir/transform/std140.cc b/src/tint/lang/core/ir/transform/std140.cc
index ed73e70..dbf0c0e 100644
--- a/src/tint/lang/core/ir/transform/std140.cc
+++ b/src/tint/lang/core/ir/transform/std140.cc
@@ -329,7 +329,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> Std140(Module* ir) {
+Result<SuccessType> Std140(Module* ir) {
     auto result = ValidateAndDumpIfNeeded(*ir, "Std140 transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/core/ir/transform/std140.h b/src/tint/lang/core/ir/transform/std140.h
index 42f12c7..768d0ac 100644
--- a/src/tint/lang/core/ir/transform/std140.h
+++ b/src/tint/lang/core/ir/transform/std140.h
@@ -29,8 +29,8 @@
 /// Std140 is a transform that rewrites matrix types in the uniform address space to conform to
 /// GLSL's std140 layout rules.
 /// @param module the module to transform
-/// @returns an error string on failure
-Result<SuccessType, std::string> Std140(Module* module);
+/// @returns success or failure
+Result<SuccessType> Std140(Module* module);
 
 }  // namespace tint::core::ir::transform
 
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index 055940c..3a2a461 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -83,9 +83,8 @@
     ~Validator();
 
     /// Runs the validator over the module provided during construction
-    /// @returns the results of validation, either a success result object or the diagnostics of
-    /// validation failures.
-    Result<SuccessType, diag::List> IsValid();
+    /// @returns success or failure
+    Result<SuccessType> Run();
 
   protected:
     /// @param inst the instruction
@@ -284,7 +283,7 @@
     mod_.disassembly_file = std::make_unique<Source::File>("", dis_.Disassemble());
 }
 
-Result<SuccessType, diag::List> Validator::IsValid() {
+Result<SuccessType> Validator::Run() {
     CheckRootBlock(mod_.root_block);
 
     for (auto* func : mod_.functions) {
@@ -295,7 +294,7 @@
         DisassembleIfNeeded();
         diagnostics_.add_note(tint::diag::System::IR,
                               "# Disassembly\n" + mod_.disassembly_file->content.data, {});
-        return std::move(diagnostics_);
+        return Failure{std::move(diagnostics_)};
     }
     return Success;
 }
@@ -863,13 +862,13 @@
 
 }  // namespace
 
-Result<SuccessType, diag::List> Validate(Module& mod) {
+Result<SuccessType> Validate(Module& mod) {
     Validator v(mod);
-    return v.IsValid();
+    return v.Run();
 }
 
-Result<SuccessType, std::string> ValidateAndDumpIfNeeded([[maybe_unused]] Module& ir,
-                                                         [[maybe_unused]] const char* msg) {
+Result<SuccessType> ValidateAndDumpIfNeeded([[maybe_unused]] Module& ir,
+                                            [[maybe_unused]] const char* msg) {
 #if TINT_DUMP_IR_WHEN_VALIDATING
     Disassembler disasm(ir);
     std::cout << "=========================================================" << std::endl;
@@ -881,10 +880,7 @@
 #ifndef NDEBUG
     auto result = Validate(ir);
     if (!result) {
-        diag::List errors;
-        StringStream ss;
-        ss << "validating input to " << msg << " failed" << std::endl << result.Failure().str();
-        return ss.str();
+        return result.Failure();
     }
 #endif
 
diff --git a/src/tint/lang/core/ir/validator.h b/src/tint/lang/core/ir/validator.h
index 497defa..4760f5d 100644
--- a/src/tint/lang/core/ir/validator.h
+++ b/src/tint/lang/core/ir/validator.h
@@ -17,7 +17,6 @@
 
 #include <string>
 
-#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations
@@ -29,14 +28,14 @@
 
 /// Validates that a given IR module is correctly formed
 /// @param mod the module to validate
-/// @returns true on success, an error result otherwise
-Result<SuccessType, diag::List> Validate(Module& mod);
+/// @returns success or failure
+Result<SuccessType> Validate(Module& mod);
 
 /// Validates the module @p ir and dumps its contents if required by the build configuration.
 /// @param ir the module to transform
 /// @param msg the msg to accompany the output
-/// @returns an error string if the module is not valid
-Result<SuccessType, std::string> ValidateAndDumpIfNeeded(Module& ir, const char* msg);
+/// @returns success or failure
+Result<SuccessType> ValidateAndDumpIfNeeded(Module& ir, const char* msg);
 
 }  // namespace tint::core::ir
 
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index 777cd01..dea28d6 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -37,7 +37,7 @@
     mod.root_block = b.RootBlock();
     mod.root_block->Append(b.Var(ty.ptr<private_, i32>()));
     auto res = ir::Validate(mod);
-    EXPECT_TRUE(res) << res.Failure().str();
+    EXPECT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, RootBlock_NonVar) {
@@ -49,7 +49,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:2:3 error: root block: invalid instruction: tint::core::ir::Loop
   loop [b: %b2] {  # loop_1
   ^^^^^^^^^^^^^
@@ -77,7 +77,7 @@
     f->Block()->Append(b.Return(f));
 
     auto res = ir::Validate(mod);
-    EXPECT_TRUE(res) << res.Failure().str();
+    EXPECT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, Function_Duplicate) {
@@ -90,7 +90,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(error: function 'my_func' added to module multiple times
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(error: function 'my_func' added to module multiple times
 note: # Disassembly
 %my_func = func(%2:i32, %3:f32):void -> %b1 {
   %b1 = block {
@@ -110,7 +111,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:2:3 error: block: does not end in a terminator instruction
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:2:3 error: block: does not end in a terminator instruction
   %b1 = block {
   ^^^^^^^^^^^
 
@@ -134,7 +136,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:25 error: access: constant index must be positive, got -1
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:3:25 error: access: constant index must be positive, got -1
     %3:f32 = access %2, -1i
                         ^^^
 
@@ -164,7 +167,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:29 error: access: index out of bounds for type vec2<f32>
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:3:29 error: access: index out of bounds for type vec2<f32>
     %3:f32 = access %2, 1u, 3u
                             ^^
 
@@ -198,7 +202,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:3:55 error: access: index out of bounds for type ptr<array<f32, 2>>
     %3:ptr<private, f32, read_write> = access %2, 1u, 3u
                                                       ^^
@@ -233,7 +237,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:25 error: access: type f32 cannot be indexed
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:25 error: access: type f32 cannot be indexed
     %3:f32 = access %2, 1u
                         ^^
 
@@ -263,7 +267,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:51 error: access: type ptr<f32> cannot be indexed
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:51 error: access: type ptr<f32> cannot be indexed
     %3:ptr<private, f32, read_write> = access %2, 1u
                                                   ^^
 
@@ -299,7 +303,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:8:25 error: access: type MyStruct cannot be dynamically indexed
     %4:i32 = access %2, %3
                         ^^
@@ -341,7 +345,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:8:25 error: access: type ptr<MyStruct> cannot be dynamically indexed
     %4:i32 = access %2, %3
                         ^^
@@ -377,7 +381,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:3:14 error: access: result of access chain is type f32 but instruction type is i32
     %3:i32 = access %2, 1u, 1u
              ^^^^^^
@@ -409,7 +413,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(
-        res.Failure().str(),
+        res.Failure().reason.str(),
         R"(:3:40 error: access: result of access chain is type ptr<f32> but instruction type is ptr<i32>
     %3:ptr<private, i32, read_write> = access %2, 1u, 1u
                                        ^^^^^^
@@ -441,7 +445,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(
-        res.Failure().str(),
+        res.Failure().reason.str(),
         R"(:3:14 error: access: result of access chain is type ptr<f32> but instruction type is f32
     %3:f32 = access %2, 1u, 1u
              ^^^^^^
@@ -472,7 +476,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:3:25 error: access: cannot obtain address of vector element
     %3:f32 = access %2, 1u
                         ^^
@@ -503,7 +507,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:3:29 error: access: cannot obtain address of vector element
     %3:f32 = access %2, 1u, 1u
                             ^^
@@ -533,7 +537,7 @@
     });
 
     auto res = ir::Validate(mod);
-    ASSERT_TRUE(res) << res.Failure().str();
+    ASSERT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, Access_IndexVector_ViaMatrix) {
@@ -547,7 +551,7 @@
     });
 
     auto res = ir::Validate(mod);
-    ASSERT_TRUE(res) << res.Failure().str();
+    ASSERT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, Block_TerminatorInMiddle) {
@@ -560,7 +564,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:3:5 error: block: terminator which isn't the final instruction
     ret
     ^^^
@@ -589,7 +593,7 @@
     f->Block()->Append(b.Return(f));
 
     auto res = ir::Validate(mod);
-    EXPECT_TRUE(res) << res.Failure().str();
+    EXPECT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, If_EmptyTrue) {
@@ -603,7 +607,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:4:7 error: block: does not end in a terminator instruction
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:4:7 error: block: does not end in a terminator instruction
       %b2 = block {  # true
       ^^^^^^^^^^^
 
@@ -635,7 +640,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:8 error: if: condition must be a `bool` type
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:8 error: if: condition must be a `bool` type
     if 1i [t: %b2, f: %b3] {  # if_1
        ^^
 
@@ -672,7 +677,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:8 error: if: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:8 error: if: operand is undefined
     if undef [t: %b2, f: %b3] {  # if_1
        ^^^^^
 
@@ -711,7 +716,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:5 error: if: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: if: instruction result is undefined
     undef = if true [t: %b2, f: %b3] {  # if_1
     ^^^^^
 
@@ -747,7 +752,7 @@
     sb.Return(f);
 
     auto res = ir::Validate(mod);
-    EXPECT_TRUE(res) << res.Failure().str();
+    EXPECT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, Loop_EmptyBody) {
@@ -759,7 +764,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:4:7 error: block: does not end in a terminator instruction
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:4:7 error: block: does not end in a terminator instruction
       %b2 = block {  # body
       ^^^^^^^^^^^
 
@@ -782,7 +788,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:2:3 error: var: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:2:3 error: var: instruction result is undefined
   undef = var
   ^^^^^
 
@@ -809,7 +815,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:5 error: var: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: var: instruction result is undefined
     undef = var
     ^^^^^
 
@@ -839,7 +845,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:41 error: var: initializer has incorrect type
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:41 error: var: initializer has incorrect type
     %2:ptr<function, f32, read_write> = var, %3
                                         ^^^
 
@@ -868,7 +874,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:5 error: let: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: let: instruction result is undefined
     undef = let 1i
     ^^^^^
 
@@ -897,7 +903,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:18 error: let: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:18 error: let: operand is undefined
     %2:f32 = let undef
                  ^^^^^
 
@@ -926,7 +932,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:14 error: let: result type does not match value type
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:14 error: let: result type does not match value type
     %2:f32 = let 1i
              ^^^
 
@@ -979,7 +985,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), expected);
+    EXPECT_EQ(res.Failure().reason.str(), expected);
 }
 
 TEST_F(IR_ValidatorTest, Instruction_NullSource) {
@@ -993,7 +999,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:5 error: var: instruction result source is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: var: instruction result source is undefined
     %2:ptr<function, f32, read_write> = var
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -1024,7 +1030,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:46 error: var: instruction has operand which is not alive
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:3:46 error: var: instruction has operand which is not alive
     %2:ptr<function, f32, read_write> = var, %3
                                              ^^
 
@@ -1055,7 +1062,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:46 error: var: instruction operand missing usage
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:46 error: var: instruction operand missing usage
     %2:ptr<function, f32, read_write> = var, %3
                                              ^^
 
@@ -1082,7 +1089,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:18 error: binary: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:18 error: binary: operand is undefined
     %2:i32 = add undef, 2i
                  ^^^^^
 
@@ -1109,7 +1116,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:22 error: binary: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:22 error: binary: operand is undefined
     %2:i32 = add 2i, undef
                      ^^^^^
 
@@ -1139,7 +1146,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:5 error: binary: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: binary: instruction result is undefined
     undef = add 3i, 2i
     ^^^^^
 
@@ -1166,7 +1173,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:23 error: unary: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:23 error: unary: operand is undefined
     %2:i32 = negation undef
                       ^^^^^
 
@@ -1196,7 +1203,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:5 error: unary: instruction result is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: unary: instruction result is undefined
     undef = negation 2i
     ^^^^^
 
@@ -1225,7 +1232,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:5 error: unary: result type must match value type
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: unary: result type must match value type
     %2:f32 = complement 2i
     ^^^^^^^^^^^^^^^^^^^^^^
 
@@ -1253,7 +1260,7 @@
     sb.Return(f);
 
     auto res = ir::Validate(mod);
-    EXPECT_TRUE(res) << res.Failure().str();
+    EXPECT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, ExitIf_NullIf) {
@@ -1267,7 +1274,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:5:9 error: exit_if: has no parent control instruction
+    EXPECT_EQ(res.Failure().reason.str(), R"(:5:9 error: exit_if: has no parent control instruction
         exit_if  # undef
         ^^^^^^^
 
@@ -1305,7 +1312,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(
-        res.Failure().str(),
+        res.Failure().reason.str(),
         R"(:5:9 error: exit_if: args count (1) does not match control instruction result count (2)
         exit_if 1i  # if_1
         ^^^^^^^^^^
@@ -1349,7 +1356,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(
-        res.Failure().str(),
+        res.Failure().reason.str(),
         R"(:5:9 error: exit_if: args count (3) does not match control instruction result count (2)
         exit_if 1i, 2.0f, 3i  # if_1
         ^^^^^^^^^^^^^^^^^^^^
@@ -1391,7 +1398,7 @@
     sb.Return(f);
 
     auto res = ir::Validate(mod);
-    ASSERT_TRUE(res) << res.Failure().str();
+    ASSERT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, ExitIf_IncorrectResultType) {
@@ -1410,7 +1417,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(
-        res.Failure().str(),
+        res.Failure().reason.str(),
         R"(:5:21 error: exit_if: argument type (f32) does not match control instruction type (i32)
         exit_if 1i, 2i  # if_1
                     ^^
@@ -1450,7 +1457,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:8:5 error: exit_if: found outside all control instructions
     exit_if  # if_1
     ^^^^^^^
@@ -1493,7 +1500,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:7:13 error: exit_if: if target jumps over other control instructions
             exit_if  # if_1
             ^^^^^^^
@@ -1546,7 +1553,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:7:13 error: exit_if: if target jumps over other control instructions
             exit_if  # if_1
             ^^^^^^^
@@ -1598,7 +1605,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:7:13 error: exit_if: if target jumps over other control instructions
             exit_if  # if_1
             ^^^^^^^
@@ -1642,7 +1649,7 @@
     sb.Return(f);
 
     auto res = ir::Validate(mod);
-    EXPECT_TRUE(res) << res.Failure().str();
+    EXPECT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, ExitSwitch_NullSwitch) {
@@ -1658,7 +1665,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:5:9 error: exit_switch: has no parent control instruction
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:5:9 error: exit_switch: has no parent control instruction
         exit_switch  # undef
         ^^^^^^^^^^^
 
@@ -1698,7 +1706,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(
-        res.Failure().str(),
+        res.Failure().reason.str(),
         R"(:5:9 error: exit_switch: args count (1) does not match control instruction result count (2)
         exit_switch 1i  # switch_1
         ^^^^^^^^^^^^^^
@@ -1742,7 +1750,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(
-        res.Failure().str(),
+        res.Failure().reason.str(),
         R"(:5:9 error: exit_switch: args count (3) does not match control instruction result count (2)
         exit_switch 1i, 2.0f, 3i  # switch_1
         ^^^^^^^^^^^^^^^^^^^^^^^^
@@ -1784,7 +1792,7 @@
     sb.Return(f);
 
     auto res = ir::Validate(mod);
-    ASSERT_TRUE(res) << res.Failure().str();
+    ASSERT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, ExitSwitch_IncorrectResultType) {
@@ -1804,7 +1812,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(
-        res.Failure().str(),
+        res.Failure().reason.str(),
         R"(:5:25 error: exit_switch: argument type (f32) does not match control instruction type (i32)
         exit_switch 1i, 2i  # switch_1
                         ^^
@@ -1848,7 +1856,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:10:9 error: exit_switch: switch not found in parent control instructions
         exit_switch  # switch_1
         ^^^^^^^^^^^
@@ -1906,7 +1914,7 @@
     sb.Return(f);
 
     auto res = ir::Validate(mod);
-    EXPECT_TRUE(res) << res.Failure().str();
+    EXPECT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, ExitSwitch_InvalidJumpOverSwitch) {
@@ -1930,7 +1938,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:7:13 error: exit_switch: switch target jumps over other control instructions
             exit_switch  # switch_1
             ^^^^^^^^^^^
@@ -1981,7 +1989,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:7:13 error: exit_switch: switch target jumps over other control instructions
             exit_switch  # switch_1
             ^^^^^^^^^^^
@@ -2024,7 +2032,7 @@
     sb.Return(f);
 
     auto res = ir::Validate(mod);
-    EXPECT_TRUE(res) << res.Failure().str();
+    EXPECT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, ExitLoop_NullLoop) {
@@ -2039,7 +2047,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:5:9 error: exit_loop: has no parent control instruction
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:5:9 error: exit_loop: has no parent control instruction
         exit_loop  # undef
         ^^^^^^^^^
 
@@ -2081,7 +2090,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(
-        res.Failure().str(),
+        res.Failure().reason.str(),
         R"(:5:9 error: exit_loop: args count (1) does not match control instruction result count (2)
         exit_loop 1i  # loop_1
         ^^^^^^^^^^^^
@@ -2128,7 +2137,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(
-        res.Failure().str(),
+        res.Failure().reason.str(),
         R"(:5:9 error: exit_loop: args count (3) does not match control instruction result count (2)
         exit_loop 1i, 2.0f, 3i  # loop_1
         ^^^^^^^^^^^^^^^^^^^^^^
@@ -2173,7 +2182,7 @@
     sb.Return(f);
 
     auto res = ir::Validate(mod);
-    ASSERT_TRUE(res) << res.Failure().str();
+    ASSERT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, ExitLoop_IncorrectResultType) {
@@ -2193,7 +2202,7 @@
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
     EXPECT_EQ(
-        res.Failure().str(),
+        res.Failure().reason.str(),
         R"(:5:23 error: exit_loop: argument type (f32) does not match control instruction type (i32)
         exit_loop 1i, 2i  # loop_1
                       ^^
@@ -2239,7 +2248,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:13:9 error: exit_loop: loop not found in parent control instructions
         exit_loop  # loop_1
         ^^^^^^^^^
@@ -2299,7 +2308,7 @@
     sb.Return(f);
 
     auto res = ir::Validate(mod);
-    EXPECT_TRUE(res) << res.Failure().str();
+    EXPECT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, ExitLoop_InvalidJumpOverSwitch) {
@@ -2323,7 +2332,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:7:13 error: exit_loop: loop target jumps over other control instructions
             exit_loop  # loop_1
             ^^^^^^^^^
@@ -2378,7 +2387,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:7:13 error: exit_loop: loop target jumps over other control instructions
             exit_loop  # loop_1
             ^^^^^^^^^
@@ -2428,7 +2437,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:8:9 error: exit_loop: loop exit jumps out of continuing block
         exit_loop  # loop_1
         ^^^^^^^^^
@@ -2474,7 +2483,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:10:13 error: exit_loop: loop exit jumps out of continuing block
             exit_loop  # loop_1
             ^^^^^^^^^
@@ -2526,7 +2535,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:5:9 error: exit_loop: loop exit not permitted in loop initializer
         exit_loop  # loop_1
         ^^^^^^^^^
@@ -2576,7 +2585,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:7:13 error: exit_loop: loop exit not permitted in loop initializer
             exit_loop  # loop_1
             ^^^^^^^^^
@@ -2621,7 +2630,7 @@
     });
 
     auto res = ir::Validate(mod);
-    EXPECT_TRUE(res) << res.Failure().str();
+    EXPECT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, Return_WithValue) {
@@ -2631,7 +2640,7 @@
     });
 
     auto res = ir::Validate(mod);
-    EXPECT_TRUE(res) << res.Failure().str();
+    EXPECT_TRUE(res) << res.Failure().reason.str();
 }
 
 TEST_F(IR_ValidatorTest, Return_NullFunction) {
@@ -2642,7 +2651,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:5 error: return: undefined function
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: return: undefined function
     ret
     ^^^
 
@@ -2667,7 +2676,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:5 error: return: unexpected return value
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: return: unexpected return value
     ret 42i
     ^^^^^^^
 
@@ -2692,7 +2701,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:5 error: return: expected return value
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:5 error: return: expected return value
     ret
     ^^^
 
@@ -2717,7 +2726,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:3:5 error: return: return value type does not match function return type
     ret 42.0f
     ^^^^^^^^^
@@ -2745,7 +2754,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:11 error: store: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:11 error: store: operand is undefined
     store undef, 42i
           ^^^^^
 
@@ -2774,7 +2783,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:4:15 error: store: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:4:15 error: store: operand is undefined
     store %2, undef
               ^^^^^
 
@@ -2804,7 +2813,8 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:4:15 error: value type does not match pointer element type
+    EXPECT_EQ(res.Failure().reason.str(),
+              R"(:4:15 error: value type does not match pointer element type
     store %2, 42u
               ^^^
 
@@ -2835,7 +2845,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(),
+    EXPECT_EQ(res.Failure().reason.str(),
               R"(:4:5 error: load_vector_element: instruction result is undefined
     undef = load_vector_element %2, 1i
     ^^^^^
@@ -2866,7 +2876,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:34 error: load_vector_element: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:34 error: load_vector_element: operand is undefined
     %2:f32 = load_vector_element undef, 1i
                                  ^^^^^
 
@@ -2896,7 +2906,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:4:38 error: load_vector_element: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:4:38 error: load_vector_element: operand is undefined
     %3:f32 = load_vector_element %2, undef
                                      ^^^^^
 
@@ -2926,7 +2936,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:3:26 error: store_vector_element: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:3:26 error: store_vector_element: operand is undefined
     store_vector_element undef, 1i, 2i
                          ^^^^^
 
@@ -2956,7 +2966,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:4:30 error: store_vector_element: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:4:30 error: store_vector_element: operand is undefined
     store_vector_element %2, undef, 2i
                              ^^^^^
 
@@ -2995,7 +3005,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_FALSE(res);
-    EXPECT_EQ(res.Failure().str(), R"(:4:34 error: store_vector_element: operand is undefined
+    EXPECT_EQ(res.Failure().reason.str(), R"(:4:34 error: store_vector_element: operand is undefined
     store_vector_element %2, 1i, undef
                                  ^^^^^
 
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
index fe63847..8412fc6 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
@@ -28,7 +28,7 @@
     ASSERT_FALSE(program.IsValid());
     auto result = Generate(program, Options{}, "");
     EXPECT_FALSE(result);
-    EXPECT_EQ(result.Failure(), "input program is not valid");
+    EXPECT_EQ(result.Failure().reason.str(), "error: make the program invalid");
 }
 
 TEST_F(GlslASTPrinterTest, Generate) {
diff --git a/src/tint/lang/glsl/writer/writer.cc b/src/tint/lang/glsl/writer/writer.cc
index 5931ad5..ca9aa99 100644
--- a/src/tint/lang/glsl/writer/writer.cc
+++ b/src/tint/lang/glsl/writer/writer.cc
@@ -22,23 +22,23 @@
 
 namespace tint::glsl::writer {
 
-Result<Output, std::string> Generate(const Program& program,
-                                     const Options& options,
-                                     const std::string& entry_point) {
+Result<Output> Generate(const Program& program,
+                        const Options& options,
+                        const std::string& entry_point) {
     if (!program.IsValid()) {
-        return std::string("input program is not valid");
+        return Failure{program.Diagnostics()};
     }
 
     // Sanitize the program.
     auto sanitized_result = Sanitize(program, options, entry_point);
     if (!sanitized_result.program.IsValid()) {
-        return sanitized_result.program.Diagnostics().str();
+        return Failure{sanitized_result.program.Diagnostics()};
     }
 
     // Generate the GLSL code.
     auto impl = std::make_unique<ASTPrinter>(sanitized_result.program, options.version);
     if (!impl->Generate()) {
-        return impl->Diagnostics().str();
+        return Failure{impl->Diagnostics()};
     }
 
     Output output;
diff --git a/src/tint/lang/glsl/writer/writer.h b/src/tint/lang/glsl/writer/writer.h
index 7b6be46..247e676 100644
--- a/src/tint/lang/glsl/writer/writer.h
+++ b/src/tint/lang/glsl/writer/writer.h
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/glsl/writer/common/options.h"
 #include "src/tint/lang/glsl/writer/output.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations
@@ -29,15 +30,15 @@
 namespace tint::glsl::writer {
 
 /// Generate GLSL for a program, according to a set of configuration options.
-/// The result will contain the GLSL and supplementary information, or an error string.
+/// The result will contain the GLSL and supplementary information, or failure.
 /// information.
 /// @param program the program to translate to GLSL
 /// @param options the configuration options to use when generating GLSL
 /// @param entry_point the entry point to generate GLSL for
-/// @returns the resulting GLSL and supplementary information, or an error string
-Result<Output, std::string> Generate(const Program& program,
-                                     const Options& options,
-                                     const std::string& entry_point);
+/// @returns the resulting GLSL and supplementary information, or failure
+Result<Output> Generate(const Program& program,
+                        const Options& options,
+                        const std::string& entry_point);
 
 }  // namespace tint::glsl::writer
 
diff --git a/src/tint/lang/glsl/writer/writer_bench.cc b/src/tint/lang/glsl/writer/writer_bench.cc
index 1c58f4b..de9d3e6 100644
--- a/src/tint/lang/glsl/writer/writer_bench.cc
+++ b/src/tint/lang/glsl/writer/writer_bench.cc
@@ -40,7 +40,7 @@
         for (auto& ep : entry_points) {
             auto res = Generate(program, {}, ep);
             if (!res) {
-                state.SkipWithError(res.Failure().c_str());
+                state.SkipWithError(res.Failure().reason.str());
             }
         }
     }
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
index d6cdd1b..f9c0147 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
@@ -28,7 +28,7 @@
     ASSERT_FALSE(program.IsValid());
     auto result = Generate(program, Options{});
     EXPECT_FALSE(result);
-    EXPECT_EQ(result.Failure(), "input program is not valid");
+    EXPECT_EQ(result.Failure().reason.str(), "error: make the program invalid");
 }
 
 TEST_F(HlslASTPrinterTest, UnsupportedExtension) {
diff --git a/src/tint/lang/hlsl/writer/writer.cc b/src/tint/lang/hlsl/writer/writer.cc
index 67493be..2c1acc8 100644
--- a/src/tint/lang/hlsl/writer/writer.cc
+++ b/src/tint/lang/hlsl/writer/writer.cc
@@ -21,21 +21,21 @@
 
 namespace tint::hlsl::writer {
 
-Result<Output, std::string> Generate(const Program& program, const Options& options) {
+Result<Output> Generate(const Program& program, const Options& options) {
     if (!program.IsValid()) {
-        return std::string("input program is not valid");
+        return Failure{program.Diagnostics()};
     }
 
     // Sanitize the program.
     auto sanitized_result = Sanitize(program, options);
     if (!sanitized_result.program.IsValid()) {
-        return sanitized_result.program.Diagnostics().str();
+        return Failure{sanitized_result.program.Diagnostics()};
     }
 
     // Generate the HLSL code.
     auto impl = std::make_unique<ASTPrinter>(sanitized_result.program);
     if (!impl->Generate()) {
-        return impl->Diagnostics().str();
+        return Failure{impl->Diagnostics()};
     }
 
     Output output;
diff --git a/src/tint/lang/hlsl/writer/writer.h b/src/tint/lang/hlsl/writer/writer.h
index 68b5c3c..658e68c 100644
--- a/src/tint/lang/hlsl/writer/writer.h
+++ b/src/tint/lang/hlsl/writer/writer.h
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/hlsl/writer/common/options.h"
 #include "src/tint/lang/hlsl/writer/output.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations
@@ -29,11 +30,11 @@
 namespace tint::hlsl::writer {
 
 /// Generate HLSL for a program, according to a set of configuration options.
-/// The result will contain the HLSL and supplementary information, or an error string.
+/// The result will contain the HLSL and supplementary information, or failure.
 /// @param program the program to translate to HLSL
 /// @param options the configuration options to use when generating HLSL
-/// @returns the resulting HLSL and supplementary information, or an error string
-Result<Output, std::string> Generate(const Program& program, const Options& options);
+/// @returns the resulting HLSL and supplementary information, or failure
+Result<Output> Generate(const Program& program, const Options& options);
 
 }  // namespace tint::hlsl::writer
 
diff --git a/src/tint/lang/hlsl/writer/writer_bench.cc b/src/tint/lang/hlsl/writer/writer_bench.cc
index 6250e0f..00c5d39 100644
--- a/src/tint/lang/hlsl/writer/writer_bench.cc
+++ b/src/tint/lang/hlsl/writer/writer_bench.cc
@@ -30,7 +30,7 @@
     for (auto _ : state) {
         auto res = Generate(program, {});
         if (!res) {
-            state.SkipWithError(res.Failure().c_str());
+            state.SkipWithError(res.Failure().reason.str());
         }
     }
 }
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
index 7087797..abdc1b7 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
@@ -32,7 +32,7 @@
     ASSERT_FALSE(program.IsValid());
     auto result = Generate(program, Options{});
     EXPECT_FALSE(result);
-    EXPECT_EQ(result.Failure(), "input program is not valid");
+    EXPECT_EQ(result.Failure().reason.str(), "error: make the program invalid");
 }
 
 TEST_F(MslASTPrinterTest, UnsupportedExtension) {
diff --git a/src/tint/lang/msl/writer/printer/helper_test.h b/src/tint/lang/msl/writer/printer/helper_test.h
index 00d1d85..4623149 100644
--- a/src/tint/lang/msl/writer/printer/helper_test.h
+++ b/src/tint/lang/msl/writer/printer/helper_test.h
@@ -72,13 +72,13 @@
     bool Generate() {
         auto raised = raise::Raise(&mod);
         if (!raised) {
-            err_ = raised.Failure();
+            err_ = raised.Failure().reason.str();
             return false;
         }
 
         auto result = writer_.Generate();
         if (!result) {
-            err_ = result.Failure();
+            err_ = result.Failure().reason.str();
             return false;
         }
         output_ = writer_.Result();
diff --git a/src/tint/lang/msl/writer/printer/printer.cc b/src/tint/lang/msl/writer/printer/printer.cc
index 77f12e3..1bb4532 100644
--- a/src/tint/lang/msl/writer/printer/printer.cc
+++ b/src/tint/lang/msl/writer/printer/printer.cc
@@ -67,7 +67,7 @@
 
 Printer::~Printer() = default;
 
-tint::Result<SuccessType, std::string> Printer::Generate() {
+tint::Result<SuccessType> Printer::Generate() {
     auto valid = core::ir::ValidateAndDumpIfNeeded(*ir_, "MSL writer");
     if (!valid) {
         return std::move(valid.Failure());
diff --git a/src/tint/lang/msl/writer/printer/printer.h b/src/tint/lang/msl/writer/printer/printer.h
index 6b395cf..1e1d84c 100644
--- a/src/tint/lang/msl/writer/printer/printer.h
+++ b/src/tint/lang/msl/writer/printer/printer.h
@@ -47,8 +47,8 @@
     explicit Printer(core::ir::Module* module);
     ~Printer() override;
 
-    /// @returns true on successful generation; false otherwise
-    tint::Result<SuccessType, std::string> Generate();
+    /// @returns success or failure
+    tint::Result<SuccessType> Generate();
 
     /// @copydoc tint::TextGenerator::Result
     std::string Result() const override;
diff --git a/src/tint/lang/msl/writer/raise/BUILD.bazel b/src/tint/lang/msl/writer/raise/BUILD.bazel
index ca4cf5b..aca5d79 100644
--- a/src/tint/lang/msl/writer/raise/BUILD.bazel
+++ b/src/tint/lang/msl/writer/raise/BUILD.bazel
@@ -32,9 +32,14 @@
     "raise.h",
   ],
   deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
     "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ],
diff --git a/src/tint/lang/msl/writer/raise/BUILD.cmake b/src/tint/lang/msl/writer/raise/BUILD.cmake
index 86f9826..6d3ab0e 100644
--- a/src/tint/lang/msl/writer/raise/BUILD.cmake
+++ b/src/tint/lang/msl/writer/raise/BUILD.cmake
@@ -31,9 +31,14 @@
 )
 
 tint_target_add_dependencies(tint_lang_msl_writer_raise lib
+  tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
   tint_utils_result
+  tint_utils_rtti
   tint_utils_text
   tint_utils_traits
 )
diff --git a/src/tint/lang/msl/writer/raise/BUILD.gn b/src/tint/lang/msl/writer/raise/BUILD.gn
index c2bcce9..b03edf1 100644
--- a/src/tint/lang/msl/writer/raise/BUILD.gn
+++ b/src/tint/lang/msl/writer/raise/BUILD.gn
@@ -31,9 +31,14 @@
     "raise.h",
   ]
   deps = [
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
     "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
     "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
     "${tint_src_dir}/utils/text",
     "${tint_src_dir}/utils/traits",
   ]
diff --git a/src/tint/lang/msl/writer/raise/raise.cc b/src/tint/lang/msl/writer/raise/raise.cc
index efd03324..bd87432 100644
--- a/src/tint/lang/msl/writer/raise/raise.cc
+++ b/src/tint/lang/msl/writer/raise/raise.cc
@@ -18,7 +18,7 @@
 
 namespace tint::msl::raise {
 
-Result<SuccessType, std::string> Raise(core::ir::Module*) {
+Result<SuccessType> Raise(core::ir::Module*) {
     // #define RUN_TRANSFORM(name)
     //     do {
     //         auto result = core::ir::transform::name(module);
diff --git a/src/tint/lang/msl/writer/raise/raise.h b/src/tint/lang/msl/writer/raise/raise.h
index bb668e7..124b5f2 100644
--- a/src/tint/lang/msl/writer/raise/raise.h
+++ b/src/tint/lang/msl/writer/raise/raise.h
@@ -17,6 +17,7 @@
 
 #include <string>
 
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations
@@ -28,8 +29,8 @@
 
 /// Raise a core IR module to the MSL dialect of the IR.
 /// @param mod the core IR module to raise to MSL dialect
-/// @returns success or an error string
-Result<SuccessType, std::string> Raise(core::ir::Module* mod);
+/// @returns success or failure
+Result<SuccessType> Raise(core::ir::Module* mod);
 
 }  // namespace tint::msl::raise
 
diff --git a/src/tint/lang/msl/writer/writer.cc b/src/tint/lang/msl/writer/writer.cc
index 088d81d..4f1e460 100644
--- a/src/tint/lang/msl/writer/writer.cc
+++ b/src/tint/lang/msl/writer/writer.cc
@@ -24,9 +24,9 @@
 
 namespace tint::msl::writer {
 
-Result<Output, std::string> Generate(const Program& program, const Options& options) {
+Result<Output> Generate(const Program& program, const Options& options) {
     if (!program.IsValid()) {
-        return std::string("input program is not valid");
+        return Failure{program.Diagnostics()};
     }
 
     Output output;
@@ -35,7 +35,7 @@
         // Convert the AST program to an IR module.
         auto converted = wgsl::reader::ProgramToIR(program);
         if (!converted) {
-            return std::string("IR converter: " + converted.Failure());
+            return converted.Failure();
         }
 
         auto ir = converted.Move();
@@ -43,7 +43,7 @@
         // Raise the IR to the MSL dialect.
         auto raised = raise::Raise(&ir);
         if (!raised) {
-            return std::move(raised.Failure());
+            return raised.Failure();
         }
 
         // Generate the MSL code.
@@ -57,7 +57,7 @@
         // Sanitize the program.
         auto sanitized_result = Sanitize(program, options);
         if (!sanitized_result.program.IsValid()) {
-            return sanitized_result.program.Diagnostics().str();
+            return Failure{sanitized_result.program.Diagnostics()};
         }
         output.needs_storage_buffer_sizes = sanitized_result.needs_storage_buffer_sizes;
         output.used_array_length_from_uniform_indices =
@@ -66,7 +66,7 @@
         // Generate the MSL code.
         auto impl = std::make_unique<ASTPrinter>(sanitized_result.program);
         if (!impl->Generate()) {
-            return impl->Diagnostics().str();
+            return Failure{impl->Diagnostics()};
         }
         output.msl = impl->Result();
         output.has_invariant_attribute = impl->HasInvariant();
diff --git a/src/tint/lang/msl/writer/writer.h b/src/tint/lang/msl/writer/writer.h
index c0ada69..28457c1 100644
--- a/src/tint/lang/msl/writer/writer.h
+++ b/src/tint/lang/msl/writer/writer.h
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/msl/writer/common/options.h"
 #include "src/tint/lang/msl/writer/output.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations
@@ -29,11 +30,11 @@
 namespace tint::msl::writer {
 
 /// Generate MSL for a program, according to a set of configuration options.
-/// The result will contain the MSL and supplementary information, or an error string.
+/// The result will contain the MSL and supplementary information, or failure.
 /// @param program the program to translate to MSL
 /// @param options the configuration options to use when generating MSL
-/// @returns the resulting MSL and supplementary information, or an error string
-Result<Output, std::string> Generate(const Program& program, const Options& options);
+/// @returns the resulting MSL and supplementary information, or failure
+Result<Output> Generate(const Program& program, const Options& options);
 
 }  // namespace tint::msl::writer
 
diff --git a/src/tint/lang/msl/writer/writer_bench.cc b/src/tint/lang/msl/writer/writer_bench.cc
index beeec4d..aa04473 100644
--- a/src/tint/lang/msl/writer/writer_bench.cc
+++ b/src/tint/lang/msl/writer/writer_bench.cc
@@ -63,7 +63,7 @@
     for (auto _ : state) {
         auto res = Generate(program, gen_options);
         if (!res) {
-            state.SkipWithError(res.Failure().c_str());
+            state.SkipWithError(res.Failure().reason.str());
         }
     }
 }
diff --git a/src/tint/lang/spirv/intrinsic/data/BUILD.bazel b/src/tint/lang/spirv/intrinsic/data/BUILD.bazel
index 8239db0..3a2673d 100644
--- a/src/tint/lang/spirv/intrinsic/data/BUILD.bazel
+++ b/src/tint/lang/spirv/intrinsic/data/BUILD.bazel
@@ -41,6 +41,7 @@
     "//src/tint/lang/core/type",
     "//src/tint/lang/spirv/type",
     "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/id",
     "//src/tint/utils/macros",
diff --git a/src/tint/lang/spirv/intrinsic/data/BUILD.cmake b/src/tint/lang/spirv/intrinsic/data/BUILD.cmake
index bfa57c3..dc7d4da 100644
--- a/src/tint/lang/spirv/intrinsic/data/BUILD.cmake
+++ b/src/tint/lang/spirv/intrinsic/data/BUILD.cmake
@@ -40,6 +40,7 @@
   tint_lang_core_type
   tint_lang_spirv_type
   tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_id
   tint_utils_macros
diff --git a/src/tint/lang/spirv/intrinsic/data/BUILD.gn b/src/tint/lang/spirv/intrinsic/data/BUILD.gn
index 642a14a..17a0d59 100644
--- a/src/tint/lang/spirv/intrinsic/data/BUILD.gn
+++ b/src/tint/lang/spirv/intrinsic/data/BUILD.gn
@@ -40,6 +40,7 @@
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/lang/spirv/type",
     "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
     "${tint_src_dir}/utils/id",
     "${tint_src_dir}/utils/macros",
diff --git a/src/tint/lang/spirv/type/BUILD.bazel b/src/tint/lang/spirv/type/BUILD.bazel
index 4337199..32ef7f6 100644
--- a/src/tint/lang/spirv/type/BUILD.bazel
+++ b/src/tint/lang/spirv/type/BUILD.bazel
@@ -37,6 +37,7 @@
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
     "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/id",
     "//src/tint/utils/macros",
diff --git a/src/tint/lang/spirv/type/BUILD.cmake b/src/tint/lang/spirv/type/BUILD.cmake
index 1e2546a..7fce4e2 100644
--- a/src/tint/lang/spirv/type/BUILD.cmake
+++ b/src/tint/lang/spirv/type/BUILD.cmake
@@ -36,6 +36,7 @@
   tint_lang_core_ir
   tint_lang_core_type
   tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_id
   tint_utils_macros
diff --git a/src/tint/lang/spirv/type/BUILD.gn b/src/tint/lang/spirv/type/BUILD.gn
index 41c6378..85fdf35 100644
--- a/src/tint/lang/spirv/type/BUILD.gn
+++ b/src/tint/lang/spirv/type/BUILD.gn
@@ -36,6 +36,7 @@
     "${tint_src_dir}/lang/core/ir",
     "${tint_src_dir}/lang/core/type",
     "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
     "${tint_src_dir}/utils/id",
     "${tint_src_dir}/utils/macros",
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
index da60198..43c18f9 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
@@ -27,7 +27,7 @@
     ASSERT_FALSE(program.IsValid());
     auto result = Generate(program, Options{});
     EXPECT_FALSE(result);
-    EXPECT_EQ(result.Failure(), "input program is not valid");
+    EXPECT_EQ(result.Failure().reason.str(), "error: make the program invalid");
 }
 
 TEST_F(SpirvASTPrinterTest, UnsupportedExtension) {
@@ -36,7 +36,7 @@
     auto program = resolver::Resolve(*this);
     auto result = Generate(program, Options{});
     EXPECT_FALSE(result);
-    EXPECT_EQ(result.Failure(),
+    EXPECT_EQ(result.Failure().reason.str(),
               R"(12:34 error: SPIR-V backend does not support extension 'undefined')");
 }
 
diff --git a/src/tint/lang/spirv/writer/common/helper_test.h b/src/tint/lang/spirv/writer/common/helper_test.h
index 5e727a9..6597f9d 100644
--- a/src/tint/lang/spirv/writer/common/helper_test.h
+++ b/src/tint/lang/spirv/writer/common/helper_test.h
@@ -105,13 +105,13 @@
     bool Generate(Printer& writer, Options options = {}) {
         auto raised = raise::Raise(&mod, options);
         if (!raised) {
-            err_ = raised.Failure();
+            err_ = raised.Failure().reason.str();
             return false;
         }
 
         auto spirv = writer.Generate();
         if (!spirv) {
-            err_ = spirv.Failure();
+            err_ = spirv.Failure().reason.str();
             return false;
         }
 
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index 35c9327..5141be9 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -153,10 +153,10 @@
 Printer::Printer(core::ir::Module* module, bool zero_init_workgroup_mem)
     : ir_(module), b_(*module), zero_init_workgroup_memory_(zero_init_workgroup_mem) {}
 
-Result<std::vector<uint32_t>, std::string> Printer::Generate() {
+Result<std::vector<uint32_t>> Printer::Generate() {
     auto valid = core::ir::ValidateAndDumpIfNeeded(*ir_, "SPIR-V writer");
     if (!valid) {
-        return std::move(valid.Failure());
+        return valid.Failure();
     }
 
     // TODO(crbug.com/tint/1906): Check supported extensions.
diff --git a/src/tint/lang/spirv/writer/printer/printer.h b/src/tint/lang/spirv/writer/printer/printer.h
index 0b061e6..b087781 100644
--- a/src/tint/lang/spirv/writer/printer/printer.h
+++ b/src/tint/lang/spirv/writer/printer/printer.h
@@ -83,8 +83,8 @@
     ///                                   storage class with OpConstantNull
     Printer(core::ir::Module* module, bool zero_init_workgroup_memory);
 
-    /// @returns the generated SPIR-V binary on success, or an error string on failure
-    tint::Result<std::vector<uint32_t>, std::string> Generate();
+    /// @returns the generated SPIR-V binary on success, or failure
+    tint::Result<std::vector<uint32_t>> Generate();
 
     /// @returns the module that this writer has produced
     writer::Module& Module() { return module_; }
diff --git a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
index 313d6fb..89f90f3 100644
--- a/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
+++ b/src/tint/lang/spirv/writer/raise/builtin_polyfill.cc
@@ -851,10 +851,10 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> BuiltinPolyfill(core::ir::Module* ir) {
+Result<SuccessType> BuiltinPolyfill(core::ir::Module* ir) {
     auto result = ValidateAndDumpIfNeeded(*ir, "BuiltinPolyfill transform");
     if (!result) {
-        return result;
+        return result.Failure();
     }
 
     State{ir}.Process();
diff --git a/src/tint/lang/spirv/writer/raise/builtin_polyfill.h b/src/tint/lang/spirv/writer/raise/builtin_polyfill.h
index 5b16292..2a3b206 100644
--- a/src/tint/lang/spirv/writer/raise/builtin_polyfill.h
+++ b/src/tint/lang/spirv/writer/raise/builtin_polyfill.h
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/core/ir/constant.h"
 #include "src/tint/lang/core/type/type.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations.
@@ -32,8 +33,8 @@
 /// BuiltinPolyfill is a transform that replaces calls to builtins with polyfills and calls to
 /// SPIR-V backend intrinsic functions.
 /// @param module the module to transform
-/// @returns an error string on failure
-Result<SuccessType, std::string> BuiltinPolyfill(core::ir::Module* module);
+/// @returns success or failure
+Result<SuccessType> BuiltinPolyfill(core::ir::Module* module);
 
 /// LiteralOperand is a type of constant value that is intended to be emitted as a literal in
 /// the SPIR-V instruction stream.
diff --git a/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc b/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
index 775bf53..23f943d 100644
--- a/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
+++ b/src/tint/lang/spirv/writer/raise/expand_implicit_splats.cc
@@ -131,10 +131,10 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> ExpandImplicitSplats(core::ir::Module* ir) {
+Result<SuccessType> ExpandImplicitSplats(core::ir::Module* ir) {
     auto result = ValidateAndDumpIfNeeded(*ir, "ExpandImplicitSplats transform");
     if (!result) {
-        return result;
+        return result.Failure();
     }
 
     Run(ir);
diff --git a/src/tint/lang/spirv/writer/raise/expand_implicit_splats.h b/src/tint/lang/spirv/writer/raise/expand_implicit_splats.h
index 18bf1d8..f9ecdb6 100644
--- a/src/tint/lang/spirv/writer/raise/expand_implicit_splats.h
+++ b/src/tint/lang/spirv/writer/raise/expand_implicit_splats.h
@@ -17,6 +17,7 @@
 
 #include <string>
 
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations.
@@ -29,8 +30,8 @@
 /// ExpandImplicitSplats is a transform that expands implicit vector splat operands in construct
 /// instructions and binary instructions where not supported by SPIR-V.
 /// @param module the module to transform
-/// @returns an error string on failure
-Result<SuccessType, std::string> ExpandImplicitSplats(core::ir::Module* module);
+/// @returns success or failure
+Result<SuccessType> ExpandImplicitSplats(core::ir::Module* module);
 
 }  // namespace tint::spirv::writer::raise
 
diff --git a/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc b/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
index 30322ec..a514416 100644
--- a/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
+++ b/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.cc
@@ -153,10 +153,10 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> HandleMatrixArithmetic(core::ir::Module* ir) {
+Result<SuccessType> HandleMatrixArithmetic(core::ir::Module* ir) {
     auto result = ValidateAndDumpIfNeeded(*ir, "HandleMatrixArithmetic transform");
     if (!result) {
-        return result;
+        return result.Failure();
     }
 
     Run(ir);
diff --git a/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.h b/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.h
index 41b4a72..1b27c9b 100644
--- a/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.h
+++ b/src/tint/lang/spirv/writer/raise/handle_matrix_arithmetic.h
@@ -17,6 +17,7 @@
 
 #include <string>
 
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations.
@@ -29,8 +30,8 @@
 /// HandleMatrixArithmetic is a transform that converts arithmetic instruction that use matrix into
 /// SPIR-V intrinsics or polyfills.
 /// @param module the module to transform
-/// @returns an error string on failure
-Result<SuccessType, std::string> HandleMatrixArithmetic(core::ir::Module* module);
+/// @returns success or failure
+Result<SuccessType> HandleMatrixArithmetic(core::ir::Module* module);
 
 }  // namespace tint::spirv::writer::raise
 
diff --git a/src/tint/lang/spirv/writer/raise/merge_return.cc b/src/tint/lang/spirv/writer/raise/merge_return.cc
index c09fdc6..ee08c71 100644
--- a/src/tint/lang/spirv/writer/raise/merge_return.cc
+++ b/src/tint/lang/spirv/writer/raise/merge_return.cc
@@ -290,7 +290,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> MergeReturn(core::ir::Module* ir) {
+Result<SuccessType> MergeReturn(core::ir::Module* ir) {
     auto result = ValidateAndDumpIfNeeded(*ir, "MergeReturn transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/spirv/writer/raise/merge_return.h b/src/tint/lang/spirv/writer/raise/merge_return.h
index 1ad6d87..abc98ad 100644
--- a/src/tint/lang/spirv/writer/raise/merge_return.h
+++ b/src/tint/lang/spirv/writer/raise/merge_return.h
@@ -17,6 +17,7 @@
 
 #include <string>
 
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations.
@@ -29,8 +30,8 @@
 /// MergeReturn is a transform merges multiple return statements in a function into a single return
 /// at the end of the function.
 /// @param module the module to transform
-/// @returns an error string on failure
-Result<SuccessType, std::string> MergeReturn(core::ir::Module* module);
+/// @returns success or failure
+Result<SuccessType> MergeReturn(core::ir::Module* module);
 
 }  // namespace tint::spirv::writer::raise
 
diff --git a/src/tint/lang/spirv/writer/raise/raise.cc b/src/tint/lang/spirv/writer/raise/raise.cc
index c4be7ea..3324eb6 100644
--- a/src/tint/lang/spirv/writer/raise/raise.cc
+++ b/src/tint/lang/spirv/writer/raise/raise.cc
@@ -34,7 +34,7 @@
 
 namespace tint::spirv::writer::raise {
 
-Result<SuccessType, std::string> Raise(core::ir::Module* module, const Options& options) {
+Result<SuccessType> Raise(core::ir::Module* module, const Options& options) {
 #define RUN_TRANSFORM(name, ...)         \
     do {                                 \
         auto result = name(__VA_ARGS__); \
diff --git a/src/tint/lang/spirv/writer/raise/raise.h b/src/tint/lang/spirv/writer/raise/raise.h
index adaae92..f1213e8 100644
--- a/src/tint/lang/spirv/writer/raise/raise.h
+++ b/src/tint/lang/spirv/writer/raise/raise.h
@@ -18,6 +18,7 @@
 #include <string>
 
 #include "src/tint/lang/spirv/writer/common/options.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations
@@ -30,8 +31,8 @@
 /// Raise a core IR module to the SPIR-V dialect of the IR.
 /// @param module the core IR module to raise to SPIR-V dialect
 /// @param options the SPIR-V writer options
-/// @returns success or an error string
-Result<SuccessType, std::string> Raise(core::ir::Module* module, const Options& options);
+/// @returns success or failure
+Result<SuccessType> Raise(core::ir::Module* module, const Options& options);
 
 }  // namespace tint::spirv::writer::raise
 
diff --git a/src/tint/lang/spirv/writer/raise/shader_io.cc b/src/tint/lang/spirv/writer/raise/shader_io.cc
index d4c60ed..4f798f3 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io.cc
+++ b/src/tint/lang/spirv/writer/raise/shader_io.cc
@@ -195,7 +195,7 @@
 };
 }  // namespace
 
-Result<SuccessType, std::string> ShaderIO(core::ir::Module* ir, const ShaderIOConfig& config) {
+Result<SuccessType> ShaderIO(core::ir::Module* ir, const ShaderIOConfig& config) {
     auto result = ValidateAndDumpIfNeeded(*ir, "ShaderIO transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/spirv/writer/raise/shader_io.h b/src/tint/lang/spirv/writer/raise/shader_io.h
index 41a2c64..ee8247c 100644
--- a/src/tint/lang/spirv/writer/raise/shader_io.h
+++ b/src/tint/lang/spirv/writer/raise/shader_io.h
@@ -17,6 +17,7 @@
 
 #include <string>
 
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations.
@@ -38,8 +39,8 @@
 /// global variables to prepare them for SPIR-V codegen.
 /// @param module the module to transform
 /// @param config the configuration
-/// @returns an error string on failure
-Result<SuccessType, std::string> ShaderIO(core::ir::Module* module, const ShaderIOConfig& config);
+/// @returns success or failure
+Result<SuccessType> ShaderIO(core::ir::Module* module, const ShaderIOConfig& config);
 
 }  // namespace tint::spirv::writer::raise
 
diff --git a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
index e749a28..99ce357 100644
--- a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
+++ b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.cc
@@ -189,7 +189,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> VarForDynamicIndex(core::ir::Module* ir) {
+Result<SuccessType> VarForDynamicIndex(core::ir::Module* ir) {
     auto result = ValidateAndDumpIfNeeded(*ir, "VarForDynamicIndex transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.h b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.h
index 6878db4..e6ca4d7 100644
--- a/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.h
+++ b/src/tint/lang/spirv/writer/raise/var_for_dynamic_index.h
@@ -17,6 +17,7 @@
 
 #include <string>
 
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations.
@@ -31,8 +32,8 @@
 /// SPIR-V writer as there is no SPIR-V instruction that can dynamically index a non-pointer
 /// composite.
 /// @param module the module to transform
-/// @returns an error string on failure
-Result<SuccessType, std::string> VarForDynamicIndex(core::ir::Module* module);
+/// @returns success or failure
+Result<SuccessType> VarForDynamicIndex(core::ir::Module* module);
 
 }  // namespace tint::spirv::writer::raise
 
diff --git a/src/tint/lang/spirv/writer/writer.cc b/src/tint/lang/spirv/writer/writer.cc
index c8181bc..e614c71 100644
--- a/src/tint/lang/spirv/writer/writer.cc
+++ b/src/tint/lang/spirv/writer/writer.cc
@@ -32,9 +32,9 @@
 Output::~Output() = default;
 Output::Output(const Output&) = default;
 
-Result<Output, std::string> Generate(const Program& program, const Options& options) {
+Result<Output> Generate(const Program& program, const Options& options) {
     if (!program.IsValid()) {
-        return std::string("input program is not valid");
+        return Failure{program.Diagnostics()};
     }
 
     bool zero_initialize_workgroup_memory =
@@ -46,7 +46,7 @@
         // Convert the AST program to an IR module.
         auto converted = wgsl::reader::ProgramToIR(program);
         if (!converted) {
-            return "IR converter: " + converted.Failure();
+            return converted.Failure();
         }
 
         auto ir = converted.Move();
@@ -74,7 +74,7 @@
         // Sanitize the program.
         auto sanitized_result = Sanitize(program, options);
         if (!sanitized_result.program.IsValid()) {
-            return sanitized_result.program.Diagnostics().str();
+            return Failure{sanitized_result.program.Diagnostics()};
         }
 
         // Generate the SPIR-V code.
@@ -82,7 +82,7 @@
             sanitized_result.program, zero_initialize_workgroup_memory,
             options.experimental_require_subgroup_uniform_control_flow);
         if (!impl->Generate()) {
-            return impl->Diagnostics().str();
+            return Failure{impl->Diagnostics()};
         }
         output.spirv = std::move(impl->Result());
     }
diff --git a/src/tint/lang/spirv/writer/writer.h b/src/tint/lang/spirv/writer/writer.h
index 7845cb9..4eceb63 100644
--- a/src/tint/lang/spirv/writer/writer.h
+++ b/src/tint/lang/spirv/writer/writer.h
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/spirv/writer/common/options.h"
 #include "src/tint/lang/spirv/writer/output.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations
@@ -29,11 +30,11 @@
 namespace tint::spirv::writer {
 
 /// Generate SPIR-V for a program, according to a set of configuration options.
-/// The result will contain the SPIR-V or an error string.
+/// The result will contain the SPIR-V or failure.
 /// @param program the program to translate to SPIR-V
 /// @param options the configuration options to use when generating SPIR-V
-/// @returns the resulting SPIR-V and supplementary information, or an error string
-Result<Output, std::string> Generate(const Program& program, const Options& options);
+/// @returns the resulting SPIR-V and supplementary information, or failure.
+Result<Output> Generate(const Program& program, const Options& options);
 
 }  // namespace tint::spirv::writer
 
diff --git a/src/tint/lang/spirv/writer/writer_bench.cc b/src/tint/lang/spirv/writer/writer_bench.cc
index b514576..adef3df 100644
--- a/src/tint/lang/spirv/writer/writer_bench.cc
+++ b/src/tint/lang/spirv/writer/writer_bench.cc
@@ -30,7 +30,7 @@
     for (auto _ : state) {
         auto res = Generate(program, options);
         if (!res) {
-            state.SkipWithError(res.Failure());
+            state.SkipWithError(res.Failure().reason.str());
         }
     }
 }
diff --git a/src/tint/lang/wgsl/ast/transform/helper_test.h b/src/tint/lang/wgsl/ast/transform/helper_test.h
index 836209a..de01f24 100644
--- a/src/tint/lang/wgsl/ast/transform/helper_test.h
+++ b/src/tint/lang/wgsl/ast/transform/helper_test.h
@@ -39,7 +39,7 @@
     wgsl::writer::Options options;
     auto result = wgsl::writer::Generate(program, options);
     if (!result) {
-        return "WGSL writer failed:\n" + result.Failure();
+        return result.Failure().reason.str();
     }
 
     auto res = result->wgsl;
diff --git a/src/tint/lang/wgsl/helpers/ir_program_test.h b/src/tint/lang/wgsl/helpers/ir_program_test.h
index 63bdd7f..615e9ee 100644
--- a/src/tint/lang/wgsl/helpers/ir_program_test.h
+++ b/src/tint/lang/wgsl/helpers/ir_program_test.h
@@ -40,17 +40,17 @@
 
     /// Build the module, cleaning up the program before returning.
     /// @returns the generated module
-    tint::Result<core::ir::Module, std::string> Build() {
+    tint::Result<core::ir::Module> Build() {
         Program program{resolver::Resolve(*this)};
         if (!program.IsValid()) {
-            return program.Diagnostics().str();
+            return Failure{program.Diagnostics()};
         }
 
         auto result = wgsl::reader::ProgramToIR(program);
         if (result) {
             auto validated = core::ir::Validate(result.Get());
             if (!validated) {
-                return validated.Failure().str();
+                return validated.Failure();
             }
         }
         return result;
@@ -59,25 +59,25 @@
     /// Build the module from the given WGSL.
     /// @param wgsl the WGSL to convert to IR
     /// @returns the generated module
-    tint::Result<core::ir::Module, std::string> Build(std::string wgsl) {
+    tint::Result<core::ir::Module> Build(std::string wgsl) {
 #if TINT_BUILD_WGSL_READER
         Source::File file("test.wgsl", std::move(wgsl));
         auto program = wgsl::reader::Parse(&file);
         if (!program.IsValid()) {
-            return program.Diagnostics().str();
+            return Failure{program.Diagnostics()};
         }
 
         auto result = wgsl::reader::ProgramToIR(program);
         if (result) {
             auto validated = core::ir::Validate(result.Get());
             if (!validated) {
-                return validated.Failure().str();
+                return validated.Failure();
             }
         }
         return result;
 #else
         (void)wgsl;
-        return std::string("error: Tint not built with the WGSL reader");
+        return Failure{"error: Tint not built with the WGSL reader"};
 #endif
     }
 
diff --git a/src/tint/lang/wgsl/ir_roundtrip_test.cc b/src/tint/lang/wgsl/ir_roundtrip_test.cc
index bbca278..7973210 100644
--- a/src/tint/lang/wgsl/ir_roundtrip_test.cc
+++ b/src/tint/lang/wgsl/ir_roundtrip_test.cc
@@ -35,7 +35,7 @@
         ASSERT_TRUE(input_program.IsValid()) << input_program.Diagnostics();
 
         auto ir_module = wgsl::reader::ProgramToIR(input_program);
-        ASSERT_TRUE(ir_module) << (ir_module ? "" : ir_module.Failure());
+        ASSERT_TRUE(ir_module) << ir_module;
 
         tint::core::ir::Disassembler d{ir_module.Get()};
         auto disassembly = d.Disassemble();
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/accessor_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/accessor_test.cc
index b2b47d3c..683e04a 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/accessor_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/accessor_test.cc
@@ -38,7 +38,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -64,7 +64,7 @@
     WrapInFunction(Decl(a), expr, expr2);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -87,7 +87,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -109,7 +109,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -132,7 +132,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -159,7 +159,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(MyStruct = struct @align(4) {
@@ -195,7 +195,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(Inner = struct @align(4) {
@@ -240,7 +240,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(Inner = struct @align(16) {
@@ -274,7 +274,7 @@
     WrapInFunction(Decl(a), assign);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -297,7 +297,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -319,7 +319,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -342,7 +342,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -373,7 +373,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(MyStruct = struct @align(16) {
@@ -403,7 +403,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -425,7 +425,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -451,7 +451,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(MyStruct = struct @align(4) {
@@ -486,7 +486,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(Inner = struct @align(4) {
@@ -530,7 +530,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(Inner = struct @align(16) {
@@ -563,7 +563,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -585,7 +585,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -607,7 +607,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -637,7 +637,7 @@
     WrapInFunction(Decl(a), expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(MyStruct = struct @align(16) {
@@ -669,7 +669,7 @@
     WrapInFunction(v, i, b);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -694,7 +694,7 @@
     WrapInFunction(v, i, b);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -719,7 +719,7 @@
     WrapInFunction(v, i, b);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/binary_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/binary_test.cc
index 1ad4372..17dbf93 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/binary_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/binary_test.cc
@@ -31,7 +31,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -54,7 +54,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, u32, read_write> = var
@@ -77,7 +77,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, u32, read_write> = var
@@ -100,7 +100,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -123,7 +123,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, i32, read_write> = var
@@ -146,7 +146,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, u32, read_write> = var
@@ -169,7 +169,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -192,7 +192,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, u32, read_write> = var
@@ -215,7 +215,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -238,7 +238,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, u32, read_write> = var
@@ -261,7 +261,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -284,7 +284,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, u32, read_write> = var
@@ -307,7 +307,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -330,7 +330,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, bool, read_write> = var
@@ -353,7 +353,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -376,7 +376,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, bool, read_write> = var
@@ -399,7 +399,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -422,7 +422,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, u32, read_write> = var
@@ -446,7 +446,7 @@
     WrapInFunction(let, expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():bool -> %b1 {
   %b1 = block {
@@ -482,7 +482,7 @@
     WrapInFunction(let, expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():bool -> %b1 {
   %b1 = block {
@@ -517,7 +517,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -540,7 +540,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -563,7 +563,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -586,7 +586,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -609,7 +609,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -632,7 +632,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -655,7 +655,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -678,7 +678,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, u32, read_write> = var
@@ -701,7 +701,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -724,7 +724,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, u32, read_write> = var
@@ -749,7 +749,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():f32 -> %b1 {
   %b1 = block {
@@ -786,7 +786,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func(%p:bool):bool -> %b1 {
   %b1 = block {
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/builtin_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/builtin_test.cc
index e3092df..ce39f3c 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/builtin_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/builtin_test.cc
@@ -31,7 +31,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %i:ptr<private, f32, read_write> = var, 1.0f
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/call_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/call_test.cc
index be35147..0fa6aea 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/call_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/call_test.cc
@@ -33,7 +33,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():f32 -> %b1 {
   %b1 = block {
@@ -58,7 +58,7 @@
          });
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%test_function = @fragment func():void -> %b1 {
   %b1 = block {
@@ -75,7 +75,7 @@
     auto* stmt = CallStmt(Call("my_func", Mul(2_a, 3_a)));
     WrapInFunction(stmt);
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func(%p:f32):void -> %b1 {
   %b1 = block {
@@ -97,7 +97,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %i:ptr<private, i32, read_write> = var, 1i
@@ -118,7 +118,7 @@
     GlobalVar("i", core::AddressSpace::kPrivate, expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %i:ptr<private, vec3<f32>, read_write> = var, vec3<f32>(0.0f)
@@ -133,7 +133,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %i:ptr<private, f32, read_write> = var, 1.0f
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/function_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/function_test.cc
index 85e02e0..24f2795 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/function_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/function_test.cc
@@ -32,7 +32,7 @@
          Vector{Builtin(core::BuiltinValue::kPosition)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%test = @vertex func():vec4<f32> [@position] -> %b1 {
   %b1 = block {
@@ -47,7 +47,7 @@
          Vector{Stage(ast::PipelineStage::kFragment)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%test = @fragment func():void -> %b1 {
   %b1 = block {
@@ -62,7 +62,7 @@
          Vector{Stage(ast::PipelineStage::kCompute), WorkgroupSize(8_i, 4_i, 2_i)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test = @compute @workgroup_size(8, 4, 2) func():void -> %b1 {
@@ -78,7 +78,7 @@
          tint::Empty);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%test = func():vec3<f32> -> %b1 {
   %b1 = block {
@@ -93,7 +93,7 @@
          Vector{If(true, Block(Return(0_f)), Else(Block(Return(1_f))))}, tint::Empty);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%test = func():f32 -> %b1 {
   %b1 = block {
@@ -117,7 +117,7 @@
          Vector{Builtin(core::BuiltinValue::kPosition)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%test = @vertex func():vec4<f32> [@position] -> %b1 {
   %b1 = block {
@@ -133,7 +133,7 @@
          Vector{Builtin(core::BuiltinValue::kPosition), Invariant()});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test = @vertex func():vec4<f32> [@invariant, @position] -> %b1 {
@@ -149,7 +149,7 @@
          Vector{Stage(ast::PipelineStage::kFragment)}, Vector{Location(1_i)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test = @fragment func():vec4<f32> [@location(1)] -> %b1 {
@@ -167,7 +167,7 @@
                                            core::InterpolationSampling::kCentroid)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(
         Disassemble(m.Get()),
@@ -185,7 +185,7 @@
          Vector{Builtin(core::BuiltinValue::kFragDepth)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%test = @fragment func():f32 [@frag_depth] -> %b1 {
   %b1 = block {
@@ -201,7 +201,7 @@
          Vector{Builtin(core::BuiltinValue::kSampleMask)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%test = @fragment func():u32 [@sample_mask] -> %b1 {
   %b1 = block {
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/let_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/let_test.cc
index 5bcd835..d341c4e 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/let_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/let_test.cc
@@ -29,7 +29,7 @@
     WrapInFunction(Let("a", Expr(42_i)));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -45,7 +45,7 @@
     WrapInFunction(Let("a", Add(1_i, 2_i)));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -63,7 +63,7 @@
                    Let("c", Expr("b")));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/literal_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/literal_test.cc
index 52a5fa2..2e964a4 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/literal_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/literal_test.cc
@@ -51,7 +51,7 @@
     GlobalVar("a", ty.bool_(), core::AddressSpace::kPrivate, expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     auto* init = GlobalVarInitializer(m.Get());
     ASSERT_TRUE(Is<core::ir::Constant>(init));
@@ -65,7 +65,7 @@
     GlobalVar("a", ty.bool_(), core::AddressSpace::kPrivate, expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     auto* init = GlobalVarInitializer(m.Get());
     ASSERT_TRUE(Is<core::ir::Constant>(init));
@@ -81,7 +81,7 @@
     GlobalVar("d", ty.bool_(), core::AddressSpace::kPrivate, Expr(false));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     auto itr = m.Get().root_block->begin();
     auto* var_a = (*itr)->As<core::ir::Var>();
@@ -109,7 +109,7 @@
     GlobalVar("a", ty.f32(), core::AddressSpace::kPrivate, expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     auto* init = GlobalVarInitializer(m.Get());
     ASSERT_TRUE(Is<core::ir::Constant>(init));
@@ -124,7 +124,7 @@
     GlobalVar("c", ty.f32(), core::AddressSpace::kPrivate, Expr(1.2_f));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     auto itr = m.Get().root_block->begin();
     auto* var_a = (*itr)->As<core::ir::Var>();
@@ -148,7 +148,7 @@
     GlobalVar("a", ty.f16(), core::AddressSpace::kPrivate, expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     auto* init = GlobalVarInitializer(m.Get());
     ASSERT_TRUE(Is<core::ir::Constant>(init));
@@ -164,7 +164,7 @@
     GlobalVar("c", ty.f16(), core::AddressSpace::kPrivate, Expr(1.2_h));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     auto itr = m.Get().root_block->begin();
     auto* var_a = (*itr)->As<core::ir::Var>();
@@ -187,7 +187,7 @@
     GlobalVar("a", ty.i32(), core::AddressSpace::kPrivate, expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     auto* init = GlobalVarInitializer(m.Get());
     ASSERT_TRUE(Is<core::ir::Constant>(init));
@@ -202,7 +202,7 @@
     GlobalVar("c", ty.i32(), core::AddressSpace::kPrivate, Expr(-2_i));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     auto itr = m.Get().root_block->begin();
     auto* var_a = (*itr)->As<core::ir::Var>();
@@ -225,7 +225,7 @@
     GlobalVar("a", ty.u32(), core::AddressSpace::kPrivate, expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     auto* init = GlobalVarInitializer(m.Get());
     ASSERT_TRUE(Is<core::ir::Constant>(init));
@@ -240,7 +240,7 @@
     GlobalVar("c", ty.u32(), core::AddressSpace::kPrivate, Expr(2_u));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     auto itr = m.Get().root_block->begin();
     auto* var_a = (*itr)->As<core::ir::Var>();
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/materialize_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/materialize_test.cc
index dcef1e4..303bc3d 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/materialize_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/materialize_test.cc
@@ -31,7 +31,7 @@
     Func("test_function", {}, ty.f32(), expr, tint::Empty);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%test_function = func():f32 -> %b1 {
   %b1 = block {
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
index fa464bb..a5cbeea 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -111,7 +111,7 @@
 namespace tint::wgsl::reader {
 namespace {
 
-using ResultType = tint::Result<core::ir::Module, diag::List>;
+using ResultType = tint::Result<core::ir::Module>;
 
 /// Impl is the private-implementation of FromProgram().
 class Impl {
@@ -251,10 +251,10 @@
         }
 
         if (diagnostics_.contains_errors()) {
-            return ResultType(std::move(diagnostics_));
+            return Failure{std::move(diagnostics_)};
         }
 
-        return ResultType{std::move(mod)};
+        return std::move(mod);
     }
 
     core::Interpolation ExtractInterpolation(const ast::InterpolateAttribute* interp) {
@@ -1417,15 +1417,15 @@
 
 }  // namespace
 
-tint::Result<core::ir::Module, std::string> ProgramToIR(const Program& program) {
+tint::Result<core::ir::Module> ProgramToIR(const Program& program) {
     if (!program.IsValid()) {
-        return std::string("input program is not valid");
+        return Failure{program.Diagnostics()};
     }
 
     Impl b(program);
     auto r = b.Build();
     if (!r) {
-        return r.Failure().str();
+        return r.Failure();
     }
 
     return r.Move();
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h
index d51f338..a4a546c 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h
@@ -18,6 +18,7 @@
 #include <string>
 
 #include "src/tint/lang/core/ir/module.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward Declarations
@@ -35,7 +36,7 @@
 /// @note this assumes the `program.IsValid()`, and has had const-eval done so
 /// any abstract values have been calculated and converted into the relevant
 /// concrete types.
-tint::Result<core::ir::Module, std::string> ProgramToIR(const Program& program);
+tint::Result<core::ir::Module> ProgramToIR(const Program& program);
 
 }  // namespace tint::wgsl::reader
 
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
index 899bd38..d80a121 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir_test.cc
@@ -60,7 +60,7 @@
     Func("f", tint::Empty, ty.void_(), tint::Empty);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     ASSERT_EQ(1u, m->functions.Length());
 
@@ -81,7 +81,7 @@
     Func("f", Vector{Param("a", ty.u32())}, ty.u32(), Vector{Return("a")});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     ASSERT_EQ(1u, m->functions.Length());
 
@@ -103,7 +103,7 @@
          ty.void_(), tint::Empty);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     ASSERT_EQ(1u, m->functions.Length());
 
@@ -124,7 +124,7 @@
     Func("f", tint::Empty, ty.void_(), tint::Empty, Vector{Stage(ast::PipelineStage::kFragment)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(m->functions[0]->Stage(), core::ir::Function::PipelineStage::kFragment);
 }
@@ -134,7 +134,7 @@
     WrapInFunction(ast_if);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
 
@@ -162,7 +162,7 @@
     WrapInFunction(ast_if);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
 
@@ -187,7 +187,7 @@
     WrapInFunction(ast_if);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
 
@@ -215,7 +215,7 @@
     WrapInFunction(ast_if);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
 
@@ -244,7 +244,7 @@
     WrapInFunction(ast_if);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
 
@@ -275,7 +275,7 @@
     WrapInFunction(ast_loop);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -308,7 +308,7 @@
     WrapInFunction(ast_loop);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -346,7 +346,7 @@
     WrapInFunction(ast_loop);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -380,7 +380,7 @@
     WrapInFunction(ast_loop);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     EXPECT_EQ(Disassemble(m),
@@ -407,7 +407,7 @@
     WrapInFunction(ast_loop);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -444,7 +444,7 @@
     WrapInFunction(ast_loop, If(true, Block(Return())));
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -485,7 +485,7 @@
     WrapInFunction(Block(ast_loop, ast_if));
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -523,7 +523,7 @@
     WrapInFunction(ast_loop);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -573,7 +573,7 @@
     WrapInFunction(ast_loop_a);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -636,7 +636,7 @@
     WrapInFunction(ast_while);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -676,7 +676,7 @@
     WrapInFunction(ast_while);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -716,7 +716,7 @@
     WrapInFunction(ast_for);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -765,7 +765,7 @@
     WrapInFunction(ast_for);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -798,7 +798,7 @@
     WrapInFunction(ast_for);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* loop = FindSingleInstruction<core::ir::Loop>(m);
@@ -830,7 +830,7 @@
     WrapInFunction(ast_switch);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* swtch = FindSingleInstruction<core::ir::Switch>(m);
@@ -881,7 +881,7 @@
     WrapInFunction(ast_switch);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* swtch = FindSingleInstruction<core::ir::Switch>(m);
@@ -920,7 +920,7 @@
     WrapInFunction(ast_switch);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* swtch = FindSingleInstruction<core::ir::Switch>(m);
@@ -953,7 +953,7 @@
     WrapInFunction(ast_switch);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
     auto* swtch = FindSingleInstruction<core::ir::Switch>(m);
@@ -996,7 +996,7 @@
     WrapInFunction(ast_switch, ast_if);
 
     auto res = Build();
-    ASSERT_TRUE(res) << (!res ? res.Failure() : "");
+    ASSERT_TRUE(res) << (!res ? res.Failure() : Failure{});
 
     auto m = res.Move();
 
@@ -1036,7 +1036,7 @@
     WrapInFunction(Ignore(Call("b")));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%b = func():i32 -> %b1 {
@@ -1060,7 +1060,7 @@
          ty.vec4<f32>(), Vector{Return("a")}, Vector{Stage(ast::PipelineStage::kFragment)},
          Vector{Location(1_i)});
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(
         Disassemble(m.Get()),
@@ -1077,7 +1077,7 @@
          Vector{Stage(ast::PipelineStage::kFragment)}, Vector{Location(1_i)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%f = @fragment func(%a:f32 [@location(2)]):f32 [@location(1)] -> %b1 {
@@ -1097,7 +1097,7 @@
          Vector{Location(1_i)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(
         Disassemble(m.Get()),
@@ -1117,7 +1117,7 @@
          Vector{Location(1_i)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(
         Disassemble(m.Get()),
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/shadowing_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/shadowing_test.cc
index 96c3cb2..0ee2cc4 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/shadowing_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/shadowing_test.cc
@@ -44,7 +44,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 S = struct @align(4) {
@@ -73,7 +73,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 S = struct @align(4) {
@@ -100,7 +100,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %b1 = block {  # root
@@ -133,7 +133,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %b1 = block {  # root
@@ -166,7 +166,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -206,7 +206,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -241,7 +241,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -288,7 +288,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -332,7 +332,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -376,7 +376,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -420,7 +420,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -468,7 +468,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -519,7 +519,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -574,7 +574,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -628,7 +628,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -679,7 +679,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -729,7 +729,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
@@ -778,7 +778,7 @@
 }
 )");
 
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ("\n" + Disassemble(m.Get()), R"(
 %f = func():i32 -> %b1 {
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/store_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/store_test.cc
index 6c6ba47..93de171 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/store_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/store_test.cc
@@ -32,7 +32,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %a:ptr<private, u32, read_write> = var
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/unary_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/unary_test.cc
index ef9ada9..7e908a4 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/unary_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/unary_test.cc
@@ -31,7 +31,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():bool -> %b1 {
   %b1 = block {
@@ -54,7 +54,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():vec4<bool> -> %b1 {
   %b1 = block {
@@ -77,7 +77,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():u32 -> %b1 {
   %b1 = block {
@@ -100,7 +100,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%my_func = func():i32 -> %b1 {
   %b1 = block {
@@ -124,7 +124,7 @@
     WrapInFunction(expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, i32, read_write> = var
@@ -148,7 +148,7 @@
     WrapInFunction(stmts);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %v1:ptr<private, i32, read_write> = var
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/var_test.cc b/src/tint/lang/wgsl/reader/program_to_ir/var_test.cc
index cd43cff..e5518c5 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/var_test.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/var_test.cc
@@ -30,7 +30,7 @@
     GlobalVar("a", ty.u32(), core::AddressSpace::kPrivate);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %a:ptr<private, u32, read_write> = var
@@ -44,7 +44,7 @@
     GlobalVar("a", ty.u32(), core::AddressSpace::kPrivate, expr);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %a:ptr<private, u32, read_write> = var, 2u
@@ -57,7 +57,7 @@
     GlobalVar("a", ty.u32(), core::AddressSpace::kStorage, Vector{Group(2_u), Binding(3_u)});
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()), R"(%b1 = block {  # root
   %a:ptr<storage, u32, read> = var @binding_point(2, 3)
@@ -71,7 +71,7 @@
     WrapInFunction(a);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -89,7 +89,7 @@
     WrapInFunction(a);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -107,7 +107,7 @@
     WrapInFunction(a, b);
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -127,7 +127,7 @@
                    Assign("a", 42_i));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -164,7 +164,7 @@
         Assign(lhs, rhs));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%f = func(%p:i32):i32 -> %b1 {
@@ -210,7 +210,7 @@
                    Assign(lhs, rhs));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%f = func(%p:i32):i32 -> %b1 {
@@ -258,7 +258,7 @@
                    Assign(lhs, rhs));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%f = func(%p:i32):i32 -> %b1 {
@@ -290,7 +290,7 @@
                    CompoundAssign("a", 42_i, core::BinaryOp::kAdd));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%test_function = @compute @workgroup_size(1, 1, 1) func():void -> %b1 {
@@ -329,7 +329,7 @@
         CompoundAssign(lhs, rhs, core::BinaryOp::kAdd));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%f = func(%p:i32):i32 -> %b1 {
@@ -381,7 +381,7 @@
                    CompoundAssign(lhs, rhs, core::BinaryOp::kAdd));
 
     auto m = Build();
-    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+    ASSERT_TRUE(m) << m;
 
     EXPECT_EQ(Disassemble(m.Get()),
               R"(%f = func(%p:i32):i32 -> %b1 {
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index 8edaf01..7bcf16d 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -1941,7 +1941,7 @@
     for (size_t i = 0, n = std::min(args.Length(), target->Parameters().Length()); i < n; i++) {
         if (!Convert(const_args[i], target->Parameters()[i]->Type(),
                      args[i]->Declaration()->source)) {
-            return tint::Failure;
+            return Failure{};
         }
     }
     return const_args;
@@ -3739,19 +3739,19 @@
 
     auto* materialized = Materialize(ValueExpression(attr->expr));
     if (!materialized) {
-        return tint::Failure;
+        return Failure{};
     }
 
     if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
         AddError("@location must be an i32 or u32 value", attr->source);
-        return tint::Failure;
+        return Failure{};
     }
 
     auto const_value = materialized->ConstantValue();
     auto value = const_value->ValueAs<AInt>();
     if (value < 0) {
         AddError("@location value must be non-negative", attr->source);
-        return tint::Failure;
+        return Failure{};
     }
 
     return static_cast<uint32_t>(value);
@@ -3763,19 +3763,19 @@
 
     auto* materialized = Materialize(ValueExpression(attr->expr));
     if (!materialized) {
-        return tint::Failure;
+        return Failure{};
     }
 
     if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
         AddError("@location must be an i32 or u32 value", attr->source);
-        return tint::Failure;
+        return Failure{};
     }
 
     auto const_value = materialized->ConstantValue();
     auto value = const_value->ValueAs<AInt>();
     if (value != 0 && value != 1) {
         AddError("@index value must be zero or one", attr->source);
-        return tint::Failure;
+        return Failure{};
     }
 
     return static_cast<uint32_t>(value);
@@ -3787,18 +3787,18 @@
 
     auto* materialized = Materialize(ValueExpression(attr->expr));
     if (!materialized) {
-        return tint::Failure;
+        return Failure{};
     }
     if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
         AddError("@binding must be an i32 or u32 value", attr->source);
-        return tint::Failure;
+        return Failure{};
     }
 
     auto const_value = materialized->ConstantValue();
     auto value = const_value->ValueAs<AInt>();
     if (value < 0) {
         AddError("@binding value must be non-negative", attr->source);
-        return tint::Failure;
+        return Failure{};
     }
     return static_cast<uint32_t>(value);
 }
@@ -3809,18 +3809,18 @@
 
     auto* materialized = Materialize(ValueExpression(attr->expr));
     if (!materialized) {
-        return tint::Failure;
+        return Failure{};
     }
     if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
         AddError("@group must be an i32 or u32 value", attr->source);
-        return tint::Failure;
+        return Failure{};
     }
 
     auto const_value = materialized->ConstantValue();
     auto value = const_value->ValueAs<AInt>();
     if (value < 0) {
         AddError("@group value must be non-negative", attr->source);
-        return tint::Failure;
+        return Failure{};
     }
     return static_cast<uint32_t>(value);
 }
@@ -3849,18 +3849,18 @@
         }
         const auto* expr = ValueExpression(value);
         if (!expr) {
-            return tint::Failure;
+            return Failure{};
         }
         auto* ty = expr->Type();
         if (!ty->IsAnyOf<core::type::I32, core::type::U32, core::type::AbstractInt>()) {
             AddError(kErrBadExpr, value->source);
-            return tint::Failure;
+            return Failure{};
         }
 
         if (expr->Stage() != core::EvaluationStage::kConstant &&
             expr->Stage() != core::EvaluationStage::kOverride) {
             AddError(kErrBadExpr, value->source);
-            return tint::Failure;
+            return Failure{};
         }
 
         args.Push(expr);
@@ -3871,7 +3871,7 @@
     if (!common_ty) {
         AddError("workgroup_size arguments must be of the same type, either i32 or u32",
                  attr->source);
-        return tint::Failure;
+        return Failure{};
     }
 
     // If all arguments are abstract-integers, then materialize to i32.
@@ -3882,12 +3882,12 @@
     for (size_t i = 0; i < args.Length(); i++) {
         auto* materialized = Materialize(args[i], common_ty);
         if (!materialized) {
-            return tint::Failure;
+            return Failure{};
         }
         if (auto* value = materialized->ConstantValue()) {
             if (value->ValueAs<AInt>() < 1) {
                 AddError("workgroup_size argument must be at least 1", values[i]->source);
-                return tint::Failure;
+                return Failure{};
             }
             ws[i] = value->ValueAs<u32>();
         } else {
@@ -3900,7 +3900,7 @@
         total_size *= static_cast<uint64_t>(ws[i].value_or(1));
         if (total_size > 0xffffffff) {
             AddError("total workgroup grid size cannot exceed 0xffffffff", values[i]->source);
-            return tint::Failure;
+            return Failure{};
         }
     }
 
@@ -3911,7 +3911,7 @@
     const ast::BuiltinAttribute* attr) {
     auto* builtin_expr = BuiltinValueExpression(attr->builtin);
     if (!builtin_expr) {
-        return tint::Failure;
+        return Failure{};
     }
     // Apply the resolved tint::sem::BuiltinEnumExpression<tint::core::BuiltinValue> to the
     // attribute.
@@ -3944,13 +3944,13 @@
     core::Interpolation out;
     auto* type = InterpolationType(attr->type);
     if (!type) {
-        return tint::Failure;
+        return Failure{};
     }
     out.type = type->Value();
     if (attr->sampling) {
         auto* sampling = InterpolationSampling(attr->sampling);
         if (!sampling) {
-            return tint::Failure;
+            return Failure{};
         }
         out.sampling = sampling->Value();
     }
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
index 4623d29..e57064a 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
@@ -100,14 +100,14 @@
         {
             auto result = RenameConflicts(&mod);
             if (!result) {
-                b.Diagnostics().add_error(diag::System::Transform, result.Failure());
+                b.Diagnostics().add(result.Failure().reason);
                 return Program(std::move(b));
             }
         }
 
         if (auto res = core::ir::Validate(mod); !res) {
             // IR module failed validation.
-            b.Diagnostics() = res.Failure();
+            b.Diagnostics() = res.Failure().reason;
             return Program{resolver::Resolve(b)};
         }
 
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.cc b/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.cc
index 7ef8cf8..23254d9 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.cc
@@ -276,7 +276,7 @@
 
 }  // namespace
 
-Result<SuccessType, std::string> RenameConflicts(core::ir::Module* ir) {
+Result<SuccessType> RenameConflicts(core::ir::Module* ir) {
     auto result = ValidateAndDumpIfNeeded(*ir, "RenameConflicts transform");
     if (!result) {
         return result;
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h b/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
index b513409..c763a54 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
+++ b/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h
@@ -17,6 +17,7 @@
 
 #include <string>
 
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations.
@@ -30,8 +31,8 @@
 /// resolving to the correct declaration, and those with identical identifiers declared in the same
 /// scope.
 /// @param module the module to transform
-/// @returns an error string on failure
-Result<SuccessType, std::string> RenameConflicts(core::ir::Module* module);
+/// @returns success or failure
+Result<SuccessType> RenameConflicts(core::ir::Module* module);
 
 }  // namespace tint::wgsl::writer
 
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc b/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc
index e5866f4..a28b301 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts_test.cc
@@ -36,7 +36,7 @@
         // Validate the input IR.
         {
             auto res = core::ir::Validate(mod);
-            EXPECT_TRUE(res) << res.Failure().str();
+            EXPECT_TRUE(res) << res.Failure().reason.str();
             if (!res) {
                 return;
             }
@@ -48,7 +48,7 @@
 
         // Validate the output IR.
         auto res = core::ir::Validate(mod);
-        EXPECT_TRUE(res) << res.Failure().str();
+        EXPECT_TRUE(res) << res.Failure().reason.str();
     }
 
     /// @returns the transformed module as a disassembled string
diff --git a/src/tint/lang/wgsl/writer/writer.cc b/src/tint/lang/wgsl/writer/writer.cc
index 48cdca0..4c82f28 100644
--- a/src/tint/lang/wgsl/writer/writer.cc
+++ b/src/tint/lang/wgsl/writer/writer.cc
@@ -26,7 +26,7 @@
 
 namespace tint::wgsl::writer {
 
-Result<Output, std::string> Generate(const Program& program, const Options& options) {
+Result<Output> Generate(const Program& program, const Options& options) {
     (void)options;
 
     Output output;
@@ -35,7 +35,7 @@
         // Generate the WGSL code.
         auto impl = std::make_unique<SyntaxTreePrinter>(program);
         if (!impl->Generate()) {
-            return impl->Diagnostics().str();
+            return Failure{impl->Diagnostics()};
         }
         output.wgsl = impl->Result();
     } else  // NOLINT(readability/braces)
@@ -44,7 +44,7 @@
         // Generate the WGSL code.
         auto impl = std::make_unique<ASTPrinter>(program);
         if (!impl->Generate()) {
-            return impl->Diagnostics().str();
+            return Failure{impl->Diagnostics()};
         }
         output.wgsl = impl->Result();
     }
diff --git a/src/tint/lang/wgsl/writer/writer.h b/src/tint/lang/wgsl/writer/writer.h
index f1e6988..d88b50d 100644
--- a/src/tint/lang/wgsl/writer/writer.h
+++ b/src/tint/lang/wgsl/writer/writer.h
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/wgsl/writer/options.h"
 #include "src/tint/lang/wgsl/writer/output.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/result/result.h"
 
 // Forward declarations
@@ -29,11 +30,11 @@
 namespace tint::wgsl::writer {
 
 /// Generate WGSL for a program, according to a set of configuration options.
-/// The result will contain the WGSL, or an error string.
+/// The result will contain the WGSL, or failure.
 /// @param program the program to translate to WGSL
 /// @param options the configuration options to use when generating WGSL
-/// @returns the resulting WGSL, or an error string
-Result<Output, std::string> Generate(const Program& program, const Options& options);
+/// @returns the resulting WGSL, or failure
+Result<Output> Generate(const Program& program, const Options& options);
 
 }  // namespace tint::wgsl::writer
 
diff --git a/src/tint/lang/wgsl/writer/writer_bench.cc b/src/tint/lang/wgsl/writer/writer_bench.cc
index c3fd5d4..05f349a 100644
--- a/src/tint/lang/wgsl/writer/writer_bench.cc
+++ b/src/tint/lang/wgsl/writer/writer_bench.cc
@@ -30,7 +30,7 @@
     for (auto _ : state) {
         auto res = Generate(program, {});
         if (!res) {
-            state.SkipWithError(res.Failure().c_str());
+            state.SkipWithError(res.Failure().reason.str());
         }
     }
 }
diff --git a/src/tint/utils/cli/BUILD.bazel b/src/tint/utils/cli/BUILD.bazel
index 3b9fcb0..5e2fb0f 100644
--- a/src/tint/utils/cli/BUILD.bazel
+++ b/src/tint/utils/cli/BUILD.bazel
@@ -33,6 +33,7 @@
   ],
   deps = [
     "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
@@ -55,6 +56,7 @@
   deps = [
     "//src/tint/utils/cli",
     "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
     "//src/tint/utils/math",
diff --git a/src/tint/utils/cli/BUILD.cmake b/src/tint/utils/cli/BUILD.cmake
index 7be4d1b..2ec8d75 100644
--- a/src/tint/utils/cli/BUILD.cmake
+++ b/src/tint/utils/cli/BUILD.cmake
@@ -32,6 +32,7 @@
 
 tint_target_add_dependencies(tint_utils_cli lib
   tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_macros
   tint_utils_math
@@ -54,6 +55,7 @@
 tint_target_add_dependencies(tint_utils_cli_test test
   tint_utils_cli
   tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_macros
   tint_utils_math
diff --git a/src/tint/utils/cli/BUILD.gn b/src/tint/utils/cli/BUILD.gn
index 12f567c..2306667 100644
--- a/src/tint/utils/cli/BUILD.gn
+++ b/src/tint/utils/cli/BUILD.gn
@@ -36,6 +36,7 @@
   ]
   deps = [
     "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
     "${tint_src_dir}/utils/macros",
     "${tint_src_dir}/utils/math",
@@ -55,6 +56,7 @@
       "${tint_src_dir}:gmock_and_gtest",
       "${tint_src_dir}/utils/cli",
       "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/ice",
       "${tint_src_dir}/utils/macros",
       "${tint_src_dir}/utils/math",
diff --git a/src/tint/utils/cli/cli.cc b/src/tint/utils/cli/cli.cc
index a8180b5..3ce5838 100644
--- a/src/tint/utils/cli/cli.cc
+++ b/src/tint/utils/cli/cli.cc
@@ -118,16 +118,14 @@
     }
 }
 
-Result<OptionSet::Unconsumed> OptionSet::Parse(std::ostream& s_err,
-                                               VectorRef<std::string_view> arguments_raw) {
+Result<OptionSet::Unconsumed> OptionSet::Parse(VectorRef<std::string_view> arguments_raw) {
     // Build a map of name to option, and set defaults
     Hashmap<std::string, Option*, 32> options_by_name;
     for (auto* opt : options.Objects()) {
         opt->SetDefault();
         for (auto name : {opt->Name(), opt->Alias(), opt->ShortName()}) {
             if (!name.empty() && !options_by_name.Add(name, opt)) {
-                s_err << "multiple options with name '" << name << "'" << std::endl;
-                return Failure;
+                return Failure{"multiple options with name '" + name + "'"};
             }
         }
     }
@@ -158,21 +156,19 @@
         }
         if (auto opt = options_by_name.Find(name)) {
             if (auto err = (*opt)->Parse(arguments); !err.empty()) {
-                s_err << err << std::endl;
-                return Failure;
+                return Failure{err};
             }
         } else {
-            s_err << "unknown flag: " << arg << std::endl;
+            StringStream err;
+            err << "unknown flag: " << arg << std::endl;
             auto names = options_by_name.Keys();
             auto alternatives =
                 Transform(names, [&](const std::string& s) { return std::string_view(s); });
-            StringStream ss;
             tint::SuggestAlternativeOptions opts;
             opts.prefix = "--";
             opts.list_possible_values = false;
-            SuggestAlternatives(arg, alternatives.Slice(), ss, opts);
-            s_err << ss.str();
-            return Failure;
+            SuggestAlternatives(arg, alternatives.Slice(), err, opts);
+            return Failure{err.str()};
         }
     }
 
diff --git a/src/tint/utils/cli/cli.h b/src/tint/utils/cli/cli.h
index 5f72859..03ae6c2 100644
--- a/src/tint/utils/cli/cli.h
+++ b/src/tint/utils/cli/cli.h
@@ -172,10 +172,9 @@
     void ShowHelp(std::ostream& out);
 
     /// Parses all the options in @p options.
-    /// @param err the error stream
     /// @param arguments the command line arguments, excluding the initial executable name
     /// @return a Result holding a list of arguments that were not consumed as options
-    Result<Unconsumed> Parse(std::ostream& err, VectorRef<std::string_view> arguments);
+    Result<Unconsumed> Parse(VectorRef<std::string_view> arguments);
 
   private:
     /// The list of options to parse
diff --git a/src/tint/utils/cli/cli_test.cc b/src/tint/utils/cli/cli_test.cc
index cfa5f25..be0f903 100644
--- a/src/tint/utils/cli/cli_test.cc
+++ b/src/tint/utils/cli/cli_test.cc
@@ -150,10 +150,8 @@
     OptionSet opts;
     auto& opt = opts.Add<BoolOption>("my_option", "a boolean value");
 
-    std::stringstream err;
-    auto res = opts.Parse(err, Split("--my_option unconsumed", " "));
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(Split("--my_option unconsumed", " "));
+    ASSERT_TRUE(res) << res;
     EXPECT_THAT(ToStringList(res.Get()), testing::ElementsAre("unconsumed"));
     EXPECT_EQ(opt.value, true);
 }
@@ -162,10 +160,8 @@
     OptionSet opts;
     auto& opt = opts.Add<BoolOption>("my_option", "a boolean value");
 
-    std::stringstream err;
-    auto res = opts.Parse(err, Split("--my_option true", " "));
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(Split("--my_option true", " "));
+    ASSERT_TRUE(res) << res;
     EXPECT_THAT(ToStringList(res.Get()), testing::ElementsAre());
     EXPECT_EQ(opt.value, true);
 }
@@ -174,10 +170,8 @@
     OptionSet opts;
     auto& opt = opts.Add<BoolOption>("my_option", "a boolean value", Default{true});
 
-    std::stringstream err;
-    auto res = opts.Parse(err, Split("--my_option false", " "));
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(Split("--my_option false", " "));
+    ASSERT_TRUE(res) << res;
     EXPECT_THAT(ToStringList(res.Get()), testing::ElementsAre());
     EXPECT_EQ(opt.value, false);
 }
@@ -186,10 +180,8 @@
     OptionSet opts;
     auto& opt = opts.Add<ValueOption<int>>("my_option", "an integer value");
 
-    std::stringstream err;
-    auto res = opts.Parse(err, Split("--my_option 42", " "));
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(Split("--my_option 42", " "));
+    ASSERT_TRUE(res) << res;
     EXPECT_THAT(ToStringList(res.Get()), testing::ElementsAre());
     EXPECT_EQ(opt.value, 42);
 }
@@ -198,10 +190,8 @@
     OptionSet opts;
     auto& opt = opts.Add<ValueOption<uint64_t>>("my_option", "a uint64_t value");
 
-    std::stringstream err;
-    auto res = opts.Parse(err, Split("--my_option 1000000", " "));
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(Split("--my_option 1000000", " "));
+    ASSERT_TRUE(res) << res;
     EXPECT_THAT(ToStringList(res.Get()), testing::ElementsAre());
     EXPECT_EQ(opt.value, 1000000);
 }
@@ -210,10 +200,8 @@
     OptionSet opts;
     auto& opt = opts.Add<ValueOption<float>>("my_option", "a float value");
 
-    std::stringstream err;
-    auto res = opts.Parse(err, Split("--my_option 1.25", " "));
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(Split("--my_option 1.25", " "));
+    ASSERT_TRUE(res) << res;
     EXPECT_THAT(ToStringList(res.Get()), testing::ElementsAre());
     EXPECT_EQ(opt.value, 1.25f);
 }
@@ -222,10 +210,8 @@
     OptionSet opts;
     auto& opt = opts.Add<StringOption>("my_option", "a string value");
 
-    std::stringstream err;
-    auto res = opts.Parse(err, Split("--my_option blah", " "));
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(Split("--my_option blah", " "));
+    ASSERT_TRUE(res) << res;
     EXPECT_THAT(ToStringList(res.Get()), testing::ElementsAre());
     EXPECT_EQ(opt.value, "blah");
 }
@@ -240,10 +226,8 @@
                                             EnumName(E::Y, "Y"),
                                             EnumName(E::Z, "Z"),
                                         });
-    std::stringstream err;
-    auto res = opts.Parse(err, Split("--my_option Y", " "));
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(Split("--my_option Y", " "));
+    ASSERT_TRUE(res) << res;
     EXPECT_THAT(ToStringList(res.Get()), testing::ElementsAre());
     EXPECT_EQ(opt.value, E::Y);
 }
@@ -252,10 +236,8 @@
     OptionSet opts;
     auto& opt = opts.Add<ValueOption<int>>("my_option", "an integer value", ShortName{"o"});
 
-    std::stringstream err;
-    auto res = opts.Parse(err, Split("-o 42", " "));
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(Split("-o 42", " "));
+    ASSERT_TRUE(res) << res;
     EXPECT_THAT(ToStringList(res.Get()), testing::ElementsAre());
     EXPECT_EQ(opt.value, 42);
 }
@@ -264,10 +246,8 @@
     OptionSet opts;
     auto& opt = opts.Add<ValueOption<int32_t>>("my_option", "a int32_t value");
 
-    std::stringstream err;
-    auto res = opts.Parse(err, Split("abc --my_option -123 def", " "));
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(Split("abc --my_option -123 def", " "));
+    ASSERT_TRUE(res) << res;
     EXPECT_THAT(ToStringList(res.Get()), testing::ElementsAre("abc", "def"));
     EXPECT_EQ(opt.value, -123);
 }
@@ -276,10 +256,8 @@
     OptionSet opts;
     auto& opt = opts.Add<ValueOption<int>>("my_option", "an int value");
 
-    std::stringstream err;
-    auto res = opts.Parse(err, Split("--my_option=123", " "));
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(Split("--my_option=123", " "));
+    ASSERT_TRUE(res) << res;
     EXPECT_THAT(ToStringList(res.Get()), testing::ElementsAre());
     EXPECT_EQ(opt.value, 123);
 }
@@ -288,10 +266,8 @@
     OptionSet opts;
     auto& opt = opts.Add<BoolOption>("my_option", "a boolean value", Default{true});
 
-    std::stringstream err;
-    auto res = opts.Parse(err, tint::Empty);
-    ASSERT_TRUE(res) << err.str();
-    EXPECT_TRUE(err.str().empty());
+    auto res = opts.Parse(tint::Empty);
+    ASSERT_TRUE(res) << res;
     EXPECT_EQ(opt.value, true);
 }
 
diff --git a/src/tint/utils/diagnostic/BUILD.bazel b/src/tint/utils/diagnostic/BUILD.bazel
index 8d4b83a..1211bf9 100644
--- a/src/tint/utils/diagnostic/BUILD.bazel
+++ b/src/tint/utils/diagnostic/BUILD.bazel
@@ -53,6 +53,12 @@
     "source.h",
   ],
   deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ],
@@ -69,7 +75,13 @@
     "source_test.cc",
   ],
   deps = [
+    "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
diff --git a/src/tint/utils/diagnostic/BUILD.cmake b/src/tint/utils/diagnostic/BUILD.cmake
index dc7251c..47e5ed5 100644
--- a/src/tint/utils/diagnostic/BUILD.cmake
+++ b/src/tint/utils/diagnostic/BUILD.cmake
@@ -37,6 +37,12 @@
 )
 
 tint_target_add_dependencies(tint_utils_diagnostic lib
+  tint_utils_containers
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_rtti
   tint_utils_text
   tint_utils_traits
 )
@@ -71,7 +77,13 @@
 )
 
 tint_target_add_dependencies(tint_utils_diagnostic_test test
+  tint_utils_containers
   tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_rtti
   tint_utils_text
   tint_utils_traits
 )
diff --git a/src/tint/utils/diagnostic/BUILD.gn b/src/tint/utils/diagnostic/BUILD.gn
index e99d811..90735ad 100644
--- a/src/tint/utils/diagnostic/BUILD.gn
+++ b/src/tint/utils/diagnostic/BUILD.gn
@@ -41,6 +41,12 @@
     "source.h",
   ]
   deps = [
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/ice",
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/rtti",
     "${tint_src_dir}/utils/text",
     "${tint_src_dir}/utils/traits",
   ]
@@ -68,7 +74,13 @@
     ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
+      "${tint_src_dir}/utils/containers",
       "${tint_src_dir}/utils/diagnostic",
+      "${tint_src_dir}/utils/ice",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/rtti",
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
diff --git a/src/tint/utils/diagnostic/diagnostic.cc b/src/tint/utils/diagnostic/diagnostic.cc
index f108c44..887554d 100644
--- a/src/tint/utils/diagnostic/diagnostic.cc
+++ b/src/tint/utils/diagnostic/diagnostic.cc
@@ -43,9 +43,4 @@
     return Formatter{style}.format(*this);
 }
 
-std::ostream& operator<<(std::ostream& out, const List& list) {
-    out << list.str();
-    return out;
-}
-
 }  // namespace tint::diag
diff --git a/src/tint/utils/diagnostic/diagnostic.h b/src/tint/utils/diagnostic/diagnostic.h
index 0fb3f36..1b40cae 100644
--- a/src/tint/utils/diagnostic/diagnostic.h
+++ b/src/tint/utils/diagnostic/diagnostic.h
@@ -19,9 +19,10 @@
 #include <ostream>
 #include <string>
 #include <utility>
-#include <vector>
 
+#include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/diagnostic/source.h"
+#include "src/tint/utils/traits/traits.h"
 
 namespace tint::diag {
 
@@ -53,6 +54,7 @@
     Type,
     Utils,
     Writer,
+    Unknown,
 };
 
 /// Diagnostic holds all the information for a single compiler diagnostic
@@ -78,9 +80,6 @@
     std::string message;
     /// system is the Tint system that raised the diagnostic.
     System system;
-    /// code is the error code, for example a validation error might have the code
-    /// `"v-0001"`.
-    const char* code = nullptr;
     /// A shared pointer to a Source::File. Only used if the diagnostic Source
     /// points to a file that was created specifically for this diagnostic
     /// (usually an ICE).
@@ -91,7 +90,7 @@
 class List {
   public:
     /// iterator is the type used for range based iteration.
-    using iterator = std::vector<Diagnostic>::const_iterator;
+    using iterator = const Diagnostic*;
 
     /// Constructs the list with no elements.
     List();
@@ -128,7 +127,7 @@
         if (diag.severity >= Severity::Error) {
             error_count_++;
         }
-        entries_.emplace_back(std::move(diag));
+        entries_.Push(std::move(diag));
     }
 
     /// adds a list of diagnostics to the end of this list.
@@ -189,25 +188,6 @@
         add(std::move(error));
     }
 
-    /// adds the error message with the given code and Source to the end of this
-    /// list.
-    /// @param system the system raising the error message
-    /// @param code the error code
-    /// @param err_msg the error message
-    /// @param source the source of the error diagnostic
-    void add_error(System system,
-                   const char* code,
-                   std::string_view err_msg,
-                   const Source& source) {
-        diag::Diagnostic error{};
-        error.code = code;
-        error.severity = diag::Severity::Error;
-        error.system = system;
-        error.source = source;
-        error.message = err_msg;
-        add(std::move(error));
-    }
-
     /// adds an internal compiler error message to the end of this list.
     /// @param system the system raising the error message
     /// @param err_msg the error message
@@ -232,11 +212,11 @@
     /// @returns the number of error diagnostics (or of higher severity).
     size_t error_count() const { return error_count_; }
     /// @returns the number of entries in the list.
-    size_t count() const { return entries_.size(); }
+    size_t count() const { return entries_.Length(); }
     /// @returns true if the diagnostics list is empty
-    bool empty() const { return entries_.empty(); }
+    bool empty() const { return entries_.IsEmpty(); }
     /// @returns the number of entrise in the diagnostics list
-    size_t size() const { return entries_.size(); }
+    size_t size() const { return entries_.Length(); }
     /// @returns the first diagnostic in the list.
     iterator begin() const { return entries_.begin(); }
     /// @returns the last diagnostic in the list.
@@ -246,7 +226,7 @@
     std::string str() const;
 
   private:
-    std::vector<Diagnostic> entries_;
+    Vector<Diagnostic, 0> entries_;
     size_t error_count_ = 0;
 };
 
@@ -254,7 +234,10 @@
 /// @param out the output stream
 /// @param list the list to emit
 /// @returns the output stream
-std::ostream& operator<<(std::ostream& out, const List& list);
+template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
+auto& operator<<(STREAM& out, const List& list) {
+    return out << list.str();
+}
 
 }  // namespace tint::diag
 
diff --git a/src/tint/utils/diagnostic/formatter.cc b/src/tint/utils/diagnostic/formatter.cc
index 8c995d6..5c9fc95 100644
--- a/src/tint/utils/diagnostic/formatter.cc
+++ b/src/tint/utils/diagnostic/formatter.cc
@@ -129,7 +129,6 @@
 void Formatter::format(const Diagnostic& diag, State& state) const {
     auto const& src = diag.source;
     auto const& rng = src.range;
-    bool has_code = diag.code != nullptr && diag.code[0] != '\0';
 
     state.set_style({Color::kDefault, true});
 
@@ -170,9 +169,6 @@
     if (style_.print_severity) {
         prefix.emplace_back(TextAndColor{to_str(diag.severity), severity_color, true});
     }
-    if (has_code) {
-        prefix.emplace_back(TextAndColor{diag.code, severity_color});
-    }
 
     for (size_t i = 0; i < prefix.size(); i++) {
         if (i > 0) {
diff --git a/src/tint/utils/diagnostic/formatter_test.cc b/src/tint/utils/diagnostic/formatter_test.cc
index 242be15..f5a23cd 100644
--- a/src/tint/utils/diagnostic/formatter_test.cc
+++ b/src/tint/utils/diagnostic/formatter_test.cc
@@ -22,17 +22,12 @@
 namespace tint::diag {
 namespace {
 
-Diagnostic Diag(Severity severity,
-                Source source,
-                std::string message,
-                System system,
-                const char* code = nullptr) {
+Diagnostic Diag(Severity severity, Source source, std::string message, System system) {
     Diagnostic d;
     d.severity = severity;
     d.source = source;
     d.message = std::move(message);
     d.system = system;
-    d.code = code;
     return d;
 }
 
@@ -64,8 +59,7 @@
     Diagnostic ascii_diag_err = Diag(Severity::Error,
                                      Source{Source::Range{{3, 16}, {3, 21}}, &ascii_file},
                                      "hiss",
-                                     System::Test,
-                                     "abc123");
+                                     System::Test);
     Diagnostic ascii_diag_ice = Diag(Severity::InternalCompilerError,
                                      Source{Source::Range{{4, 16}, {4, 19}}, &ascii_file},
                                      "unreachable",
@@ -86,8 +80,7 @@
     Diagnostic utf8_diag_err = Diag(Severity::Error,
                                     Source{Source::Range{{3, 15}, {3, 20}}, &utf8_file},
                                     "hiss",
-                                    System::Test,
-                                    "abc123");
+                                    System::Test);
     Diagnostic utf8_diag_ice = Diag(Severity::InternalCompilerError,
                                     Source{Source::Range{{4, 15}, {4, 18}}, &utf8_file},
                                     "unreachable",
@@ -103,7 +96,7 @@
     auto got = fmt.format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
     auto* expect = R"(1:14: purr
 2:14: grrr
-3:16 abc123: hiss)";
+3:16: hiss)";
     ASSERT_EQ(expect, got);
 }
 
@@ -112,7 +105,7 @@
     auto got = fmt.format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
     auto* expect = R"(1:14: purr
 2:14: grrr
-3:16 abc123: hiss
+3:16: hiss
 )";
     ASSERT_EQ(expect, got);
 }
@@ -130,7 +123,7 @@
     auto got = fmt.format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
     auto* expect = R"(file.name:1:14: purr
 file.name:2:14: grrr
-file.name:3:16 abc123: hiss)";
+file.name:3:16: hiss)";
     ASSERT_EQ(expect, got);
 }
 
@@ -139,7 +132,7 @@
     auto got = fmt.format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
     auto* expect = R"(1:14 note: purr
 2:14 warning: grrr
-3:16 error abc123: hiss)";
+3:16 error: hiss)";
     ASSERT_EQ(expect, got);
 }
 
@@ -154,7 +147,7 @@
 the  dog  says  woof
                 ^^^^
 
-3:16 abc123: hiss
+3:16: hiss
 the  snake  says  quack
                   ^^^^^
 )";
@@ -171,7 +164,7 @@
         "2:15: grrr\n"
         "the  \xf0\x9f\x90\x95  says  woof\n"
         "\n"
-        "3:15 abc123: hiss\n"
+        "3:15: hiss\n"
         "the  \xf0\x9f\x90\x8d  says  quack\n";
     ASSERT_EQ(expect, got);
 }
@@ -187,7 +180,7 @@
 the  dog  says  woof
                 ^^^^
 
-file.name:3:16 error abc123: hiss
+file.name:3:16 error: hiss
 the  snake  says  quack
                   ^^^^^
 )";
@@ -234,7 +227,7 @@
 the    dog    says    woof
                       ^^^^
 
-file.name:3:16 error abc123: hiss
+file.name:3:16 error: hiss
 the    snake    says    quack
                         ^^^^^
 )";
diff --git a/src/tint/utils/generator/BUILD.bazel b/src/tint/utils/generator/BUILD.bazel
index 7885b9e..6f474ef 100644
--- a/src/tint/utils/generator/BUILD.bazel
+++ b/src/tint/utils/generator/BUILD.bazel
@@ -36,6 +36,9 @@
     "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ],
diff --git a/src/tint/utils/generator/BUILD.cmake b/src/tint/utils/generator/BUILD.cmake
index b05cf02..7d8a660 100644
--- a/src/tint/utils/generator/BUILD.cmake
+++ b/src/tint/utils/generator/BUILD.cmake
@@ -35,6 +35,9 @@
   tint_utils_diagnostic
   tint_utils_ice
   tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_rtti
   tint_utils_text
   tint_utils_traits
 )
diff --git a/src/tint/utils/generator/BUILD.gn b/src/tint/utils/generator/BUILD.gn
index e1d134c..cf3680b 100644
--- a/src/tint/utils/generator/BUILD.gn
+++ b/src/tint/utils/generator/BUILD.gn
@@ -35,6 +35,9 @@
     "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
     "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/rtti",
     "${tint_src_dir}/utils/text",
     "${tint_src_dir}/utils/traits",
   ]
diff --git a/src/tint/utils/result/BUILD.bazel b/src/tint/utils/result/BUILD.bazel
index 56456e4..9c591ed 100644
--- a/src/tint/utils/result/BUILD.bazel
+++ b/src/tint/utils/result/BUILD.bazel
@@ -32,8 +32,13 @@
     "result.h",
   ],
   deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/rtti",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
   ],
@@ -47,9 +52,14 @@
     "result_test.cc",
   ],
   deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
     "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@gtest",
diff --git a/src/tint/utils/result/BUILD.cmake b/src/tint/utils/result/BUILD.cmake
index 13ab5a3..cd482ee 100644
--- a/src/tint/utils/result/BUILD.cmake
+++ b/src/tint/utils/result/BUILD.cmake
@@ -31,8 +31,13 @@
 )
 
 tint_target_add_dependencies(tint_utils_result lib
+  tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_rtti
   tint_utils_text
   tint_utils_traits
 )
@@ -46,9 +51,14 @@
 )
 
 tint_target_add_dependencies(tint_utils_result_test test
+  tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
   tint_utils_result
+  tint_utils_rtti
   tint_utils_text
   tint_utils_traits
 )
diff --git a/src/tint/utils/result/BUILD.gn b/src/tint/utils/result/BUILD.gn
index 4baa0ea..f607347 100644
--- a/src/tint/utils/result/BUILD.gn
+++ b/src/tint/utils/result/BUILD.gn
@@ -35,8 +35,13 @@
     "result.h",
   ]
   deps = [
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
     "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
+    "${tint_src_dir}/utils/rtti",
     "${tint_src_dir}/utils/text",
     "${tint_src_dir}/utils/traits",
   ]
@@ -47,9 +52,14 @@
     sources = [ "result_test.cc" ]
     deps = [
       "${tint_src_dir}:gmock_and_gtest",
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/ice",
       "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
       "${tint_src_dir}/utils/result",
+      "${tint_src_dir}/utils/rtti",
       "${tint_src_dir}/utils/text",
       "${tint_src_dir}/utils/traits",
     ]
diff --git a/src/tint/utils/result/result.cc b/src/tint/utils/result/result.cc
index add6f74..5bb029e 100644
--- a/src/tint/utils/result/result.cc
+++ b/src/tint/utils/result/result.cc
@@ -14,9 +14,14 @@
 
 #include "src/tint/utils/result/result.h"
 
-#if defined(__clang__)
-#pragma clang diagnostic ignored "-Wmissing-variable-declarations"
-#endif
+namespace tint {
 
-// A placeholder symbol used to emit a symbol for this lib target.
-int tint_utils_result_symbol = 1;
+Failure::Failure() = default;
+
+Failure::Failure(std::string_view err) {
+    reason.add_error(diag::System::Unknown, err, Source{});
+}
+
+Failure::Failure(diag::List diagnostics) : reason(diagnostics) {}
+
+}  // namespace tint
diff --git a/src/tint/utils/result/result.h b/src/tint/utils/result/result.h
index f79468b..8e51b44 100644
--- a/src/tint/utils/result/result.h
+++ b/src/tint/utils/result/result.h
@@ -18,6 +18,7 @@
 #include <utility>
 #include <variant>
 
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/ice/ice.h"
 #include "src/tint/utils/text/string_stream.h"
 #include "src/tint/utils/traits/traits.h"
@@ -30,11 +31,31 @@
 /// An instance of SuccessType that can be used as a generic success value for a Result.
 static constexpr const SuccessType Success;
 
-/// Empty structure used as the default FAILURE_TYPE for a Result.
-struct FailureType {};
+/// The default Result error type.
+struct Failure {
+    /// Constructor with no diagnostics
+    Failure();
 
-/// An instance of FailureType which can be used as a generic failure value by Result
-static constexpr const FailureType Failure;
+    /// Constructor with a single diagnostic
+    /// @param err the single error diagnostic
+    explicit Failure(std::string_view err);
+
+    /// Constructor with a list of diagnostics
+    /// @param diagnostics the failure diagnostics
+    explicit Failure(diag::List diagnostics);
+
+    /// The diagnostics explaining the failure reason
+    diag::List reason;
+};
+
+/// Write the Failure to the given stream
+/// @param out the output stream
+/// @param failure the Failure
+/// @returns the output stream
+template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
+auto& operator<<(STREAM& out, const Failure& failure) {
+    return out << failure.reason;
+}
 
 /// Result is a helper for functions that need to return a value, or an failure value.
 /// Result can be constructed with either a 'success' or 'failure' value.
@@ -42,7 +63,7 @@
 /// @tparam FAILURE_TYPE the 'failure' value type. Defaults to FailureType which provides no
 ///         information about the failure, except that something failed. Must not be the same type
 ///         as SUCCESS_TYPE.
-template <typename SUCCESS_TYPE, typename FAILURE_TYPE = FailureType>
+template <typename SUCCESS_TYPE, typename FAILURE_TYPE = Failure>
 struct [[nodiscard]] Result {
     static_assert(!std::is_same_v<SUCCESS_TYPE, FAILURE_TYPE>,
                   "Result must not have the same type for SUCCESS_TYPE and FAILURE_TYPE");
@@ -168,8 +189,20 @@
           typename SUCCESS,
           typename FAILURE,
           typename = traits::EnableIfIsOStream<STREAM>>
-auto& operator<<(STREAM& out, Result<SUCCESS, FAILURE> res) {
-    return res ? (out << "success: " << res.Get()) : (out << "failure: " << res.Failure());
+auto& operator<<(STREAM& out, const Result<SUCCESS, FAILURE>& res) {
+    if (res) {
+        if constexpr (traits::HasOperatorShiftLeft<STREAM&, SUCCESS>) {
+            return out << "success: " << res.Get();
+        } else {
+            return out << "success";
+        }
+    } else {
+        if constexpr (traits::HasOperatorShiftLeft<STREAM&, FAILURE>) {
+            return out << "failure: " << res.Failure();
+        } else {
+            return out << "failure";
+        }
+    }
 }
 
 }  // namespace tint
diff --git a/src/tint/utils/result/result_test.cc b/src/tint/utils/result/result_test.cc
index 35ed8cf..6f01df8 100644
--- a/src/tint/utils/result/result_test.cc
+++ b/src/tint/utils/result/result_test.cc
@@ -39,7 +39,7 @@
 }
 
 TEST(ResultTest, Failure) {
-    auto r = Result<int>(Failure);
+    auto r = Result<int>(Failure{});
     EXPECT_FALSE(r);
     EXPECT_TRUE(!r);
 }
diff --git a/src/tint/utils/strconv/BUILD.bazel b/src/tint/utils/strconv/BUILD.bazel
index 74331d3..86eefce 100644
--- a/src/tint/utils/strconv/BUILD.bazel
+++ b/src/tint/utils/strconv/BUILD.bazel
@@ -34,9 +34,14 @@
     "parse_num.h",
   ],
   deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
     "//src/tint/utils/ice",
     "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
     "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
     "//src/tint/utils/text",
     "//src/tint/utils/traits",
     "@abseil_cpp//absl/strings",
diff --git a/src/tint/utils/strconv/BUILD.cmake b/src/tint/utils/strconv/BUILD.cmake
index abb1139..47ccddc 100644
--- a/src/tint/utils/strconv/BUILD.cmake
+++ b/src/tint/utils/strconv/BUILD.cmake
@@ -33,9 +33,14 @@
 )
 
 tint_target_add_dependencies(tint_utils_strconv lib
+  tint_utils_containers
+  tint_utils_diagnostic
   tint_utils_ice
   tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
   tint_utils_result
+  tint_utils_rtti
   tint_utils_text
   tint_utils_traits
 )
diff --git a/src/tint/utils/strconv/BUILD.gn b/src/tint/utils/strconv/BUILD.gn
index 11b33f6..e386dcb 100644
--- a/src/tint/utils/strconv/BUILD.gn
+++ b/src/tint/utils/strconv/BUILD.gn
@@ -38,9 +38,14 @@
   ]
   deps = [
     "${tint_src_dir}:abseil",
+    "${tint_src_dir}/utils/containers",
+    "${tint_src_dir}/utils/diagnostic",
     "${tint_src_dir}/utils/ice",
     "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/math",
+    "${tint_src_dir}/utils/memory",
     "${tint_src_dir}/utils/result",
+    "${tint_src_dir}/utils/rtti",
     "${tint_src_dir}/utils/text",
     "${tint_src_dir}/utils/traits",
   ]
diff --git a/src/tint/utils/traits/traits.h b/src/tint/utils/traits/traits.h
index 62071c7..ff1a24d 100644
--- a/src/tint/utils/traits/traits.h
+++ b/src/tint/utils/traits/traits.h
@@ -209,6 +209,9 @@
 template <typename T>
 using CharArrayToCharPtr = typename traits::detail::CharArrayToCharPtrImpl<T>::type;
 
+////////////////////////////////////////////////////////////////////////////////
+// IsOStream
+////////////////////////////////////////////////////////////////////////////////
 namespace detail {
 /// Helper for determining whether the type T can be used as a stream writer
 template <typename T, typename ENABLE = void>
@@ -239,6 +242,26 @@
 template <typename T = void>
 using EnableIfIsOStream = EnableIf<IsOStream<T>, T>;
 
+////////////////////////////////////////////////////////////////////////////////
+// HasOperatorShiftLeft
+////////////////////////////////////////////////////////////////////////////////
+namespace detail {
+/// Helper for determining whether the operator<<(LHS, RHS) exists
+template <typename LHS, typename RHS, typename = void>
+struct HasOperatorShiftLeft : std::false_type {};
+/// Specialization to detect operator
+template <typename LHS, typename RHS>
+struct HasOperatorShiftLeft<LHS,
+                            RHS,
+                            std::void_t<decltype((std::declval<LHS>() << std::declval<RHS>()))>>
+    : std::true_type {};
+
+}  // namespace detail
+
+/// Is true if operator<<(LHS, RHS) exists
+template <typename LHS, typename RHS>
+static constexpr bool HasOperatorShiftLeft = detail::HasOperatorShiftLeft<LHS, RHS>::value;
+
 }  // namespace tint::traits
 
 #endif  // SRC_TINT_UTILS_TRAITS_TRAITS_H_