Implement error reporting for the bindings generator.

This feature aggregates nonfatal errors encountered while generating bindings and writes a JSON report to a file. It can be activated by passing `--//path/to/crubit/bazel_support:generate_error_report` to a Bazel build that generates bindings. The error report is named `${cc_library}_rust_api_error_report.json` and appears in the output directory alongside the generated bindings.

PiperOrigin-RevId: 481263077
diff --git a/common/arc_anyhow.rs b/common/arc_anyhow.rs
index 159ec68..7ed7be9 100644
--- a/common/arc_anyhow.rs
+++ b/common/arc_anyhow.rs
@@ -54,6 +54,13 @@
     {
         self.into_anyhow().context(context).into()
     }
+
+    pub fn downcast_ref<E>(&self) -> Option<&E>
+    where
+        E: Display + Debug + Send + Sync + 'static,
+    {
+        self.0.downcast_ref()
+    }
 }
 
 impl PartialEq for Error {
diff --git a/rs_bindings_from_cc/BUILD b/rs_bindings_from_cc/BUILD
index 9b79269..43a7068 100644
--- a/rs_bindings_from_cc/BUILD
+++ b/rs_bindings_from_cc/BUILD
@@ -290,6 +290,7 @@
     deps = [
         "//common:arc_anyhow",
         "@crate_index//:itertools",
+        "@crate_index//:once_cell",
         "@crate_index//:proc-macro2",
         "@crate_index//:quote",
         "@crate_index//:serde",
@@ -382,6 +383,7 @@
     name = "src_code_gen_impl",
     srcs = ["src_code_gen.rs"],
     deps = [
+        ":error_report",
         ":ir",
         "//common:arc_anyhow",
         "//common:code_gen_utils",
@@ -411,6 +413,25 @@
     ],
 )
 
+rust_library(
+    name = "error_report",
+    srcs = ["error_report.rs"],
+    deps = [
+        "//common:arc_anyhow",
+        "@crate_index//:anyhow",
+        "@crate_index//:serde",
+        "@crate_index//:serde_json",
+    ],
+)
+
+rust_test(
+    name = "error_report_test",
+    crate = ":error_report",
+    deps = [
+        "@crate_index//:serde_json",
+    ],
+)
+
 cc_library(
     name = "ast_convert",
     srcs = ["ast_convert.cc"],
diff --git a/rs_bindings_from_cc/bazel_support/BUILD b/rs_bindings_from_cc/bazel_support/BUILD
index 2477435..0f69d18 100644
--- a/rs_bindings_from_cc/bazel_support/BUILD
+++ b/rs_bindings_from_cc/bazel_support/BUILD
@@ -164,3 +164,9 @@
 deps_for_bindings(
     name = "empty_deps",
 )
+
+bool_flag(
+    name = "generate_error_report",
+    build_setting_default = False,
+    visibility = ["//visibility:public"],
+)
diff --git a/rs_bindings_from_cc/bazel_support/generate_bindings.bzl b/rs_bindings_from_cc/bazel_support/generate_bindings.bzl
index eee086c..8ecb82d 100644
--- a/rs_bindings_from_cc/bazel_support/generate_bindings.bzl
+++ b/rs_bindings_from_cc/bazel_support/generate_bindings.bzl
@@ -9,6 +9,7 @@
 """
 
 load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 
 def _get_hdrs_command_line(hdrs):
     return ["--public_headers=" + ",".join([x.path for x in hdrs])]
@@ -43,11 +44,34 @@
       extra_rs_srcs: A list of extra source files to add.
 
     Returns:
-      tuple(cc_output, rs_output): The generated source files.
+      tuple(cc_output, rs_output, namespaces_output, error_report_output): The generated source files.
     """
     cc_output = ctx.actions.declare_file(ctx.label.name + "_rust_api_impl.cc")
     rs_output = ctx.actions.declare_file(ctx.label.name + "_rust_api.rs")
     namespaces_output = ctx.actions.declare_file(ctx.label.name + "_namespaces.json")
+    error_report_output = None
+
+    rs_bindings_from_cc_flags = [
+        "--stderrthreshold=2",
+        "--rs_out",
+        rs_output.path,
+        "--cc_out",
+        cc_output.path,
+        "--namespaces_out",
+        namespaces_output.path,
+        "--crubit_support_path",
+        "rs_bindings_from_cc/support",
+        "--rustfmt_exe_path",
+        "third_party/unsupported_toolchains/rust/toolchains/nightly/bin/rustfmt",
+        "--rustfmt_config_path",
+        "nowhere/rustfmt.toml",
+    ]
+    if ctx.attr._generate_error_report[BuildSettingInfo].value:
+        error_report_output = ctx.actions.declare_file(ctx.label.name + "_rust_api_error_report.json")
+        rs_bindings_from_cc_flags += [
+            "--error_report_out",
+            error_report_output.path,
+        ]
 
     variables = cc_common.create_compile_variables(
         feature_configuration = feature_configuration,
@@ -73,21 +97,7 @@
         preprocessor_defines = compilation_context.defines,
         variables_extension = {
             "rs_bindings_from_cc_tool": ctx.executable._generator.path,
-            "rs_bindings_from_cc_flags": [
-                "--stderrthreshold=2",
-                "--rs_out",
-                rs_output.path,
-                "--cc_out",
-                cc_output.path,
-                "--namespaces_out",
-                namespaces_output.path,
-                "--crubit_support_path",
-                "rs_bindings_from_cc/support",
-                "--rustfmt_exe_path",
-                "third_party/unsupported_toolchains/rust/toolchains/nightly/bin/rustfmt",
-                "--rustfmt_config_path",
-                "nowhere/rustfmt.toml",
-            ] + _get_hdrs_command_line(public_hdrs) + _get_extra_rs_srcs_command_line(extra_rs_srcs),
+            "rs_bindings_from_cc_flags": rs_bindings_from_cc_flags + _get_hdrs_command_line(public_hdrs) + _get_extra_rs_srcs_command_line(extra_rs_srcs),
             "targets_and_headers": targets_and_headers,
         },
     )
@@ -109,7 +119,7 @@
             ] + ctx.files._rustfmt_cfg + extra_rs_srcs,
             transitive = [action_inputs],
         ),
-        additional_outputs = [rs_output, namespaces_output],
+        additional_outputs = [x for x in [rs_output, namespaces_output, error_report_output] if x != None],
         variables = variables,
     )
-    return (cc_output, rs_output, namespaces_output)
+    return (cc_output, rs_output, namespaces_output, error_report_output)
diff --git a/rs_bindings_from_cc/bazel_support/rust_bindings_from_cc_utils.bzl b/rs_bindings_from_cc/bazel_support/rust_bindings_from_cc_utils.bzl
index 77efc12..13983dc 100644
--- a/rs_bindings_from_cc/bazel_support/rust_bindings_from_cc_utils.bzl
+++ b/rs_bindings_from_cc/bazel_support/rust_bindings_from_cc_utils.bzl
@@ -59,7 +59,7 @@
         unsupported_features = ctx.disabled_features + ["module_maps"],
     )
 
-    cc_output, rs_output, namespaces_output = generate_bindings(
+    cc_output, rs_output, namespaces_output, error_report_output = generate_bindings(
         ctx = ctx,
         attr = attr,
         cc_toolchain = cc_toolchain,
@@ -111,7 +111,7 @@
             rust_file = rs_output,
             namespaces_file = namespaces_output,
         ),
-        OutputGroupInfo(out = depset([cc_output, rs_output, namespaces_output])),
+        OutputGroupInfo(out = depset([x for x in [cc_output, rs_output, namespaces_output, error_report_output] if x != None])),
     ]
 
 bindings_attrs = {
@@ -158,4 +158,7 @@
     "_builtin_hdrs": attr.label(
         default = "//rs_bindings_from_cc:builtin_headers",
     ),
+    "_generate_error_report": attr.label(
+        default = "//rs_bindings_from_cc/bazel_support:generate_error_report",
+    ),
 }
diff --git a/rs_bindings_from_cc/cmdline.cc b/rs_bindings_from_cc/cmdline.cc
index 461ccaf..0cef578 100644
--- a/rs_bindings_from_cc/cmdline.cc
+++ b/rs_bindings_from_cc/cmdline.cc
@@ -63,6 +63,8 @@
 ABSL_FLAG(std::string, namespaces_out, "",
           "(optional) output path for the JSON file containing the target's"
           "namespace hierarchy.");
+ABSL_FLAG(std::string, error_report_out, "",
+          "(optional) output path for the JSON error report");
 
 namespace crubit {
 
@@ -92,7 +94,8 @@
       absl::GetFlag(FLAGS_targets_and_headers),
       absl::GetFlag(FLAGS_extra_rs_srcs),
       absl::GetFlag(FLAGS_srcs_to_scan_for_instantiations),
-      absl::GetFlag(FLAGS_instantiations_out));
+      absl::GetFlag(FLAGS_instantiations_out),
+      absl::GetFlag(FLAGS_error_report_out));
 }
 
 absl::StatusOr<Cmdline> Cmdline::CreateFromArgs(
@@ -102,7 +105,7 @@
     bool do_nothing, std::vector<std::string> public_headers,
     std::string targets_and_headers_str, std::vector<std::string> extra_rs_srcs,
     std::vector<std::string> srcs_to_scan_for_instantiations,
-    std::string instantiations_out) {
+    std::string instantiations_out, std::string error_report_out) {
   Cmdline cmdline;
 
   if (rs_out.empty()) {
@@ -149,6 +152,7 @@
   cmdline.instantiations_out_ = std::move(instantiations_out);
   cmdline.srcs_to_scan_for_instantiations_ =
       std::move(srcs_to_scan_for_instantiations);
+  cmdline.error_report_out_ = std::move(error_report_out);
 
   if (targets_and_headers_str.empty()) {
     return absl::InvalidArgumentError("please specify --targets_and_headers");
diff --git a/rs_bindings_from_cc/cmdline.h b/rs_bindings_from_cc/cmdline.h
index 7b6282d..af89c97 100644
--- a/rs_bindings_from_cc/cmdline.h
+++ b/rs_bindings_from_cc/cmdline.h
@@ -32,14 +32,14 @@
       std::string targets_and_headers_str,
       std::vector<std::string> extra_rs_sources,
       std::vector<std::string> srcs_to_scan_for_instantiations,
-      std::string instantiations_out) {
+      std::string instantiations_out, std::string error_report_out) {
     return CreateFromArgs(
         std::move(cc_out), std::move(rs_out), std::move(ir_out),
         std::move(namespaces_out), std::move(crubit_support_path),
         std::move(rustfmt_exe_path), std::move(rustfmt_config_path), do_nothing,
         std::move(public_headers), std::move(targets_and_headers_str),
         std::move(extra_rs_sources), std::move(srcs_to_scan_for_instantiations),
-        std::move(instantiations_out));
+        std::move(instantiations_out), std::move(error_report_out));
   }
 
   Cmdline(const Cmdline&) = delete;
@@ -55,6 +55,7 @@
   absl::string_view rustfmt_exe_path() const { return rustfmt_exe_path_; }
   absl::string_view rustfmt_config_path() const { return rustfmt_config_path_; }
   absl::string_view instantiations_out() const { return instantiations_out_; }
+  absl::string_view error_report_out() const { return error_report_out_; }
   bool do_nothing() const { return do_nothing_; }
 
   const std::vector<HeaderName>& public_headers() const {
@@ -87,7 +88,7 @@
       std::string targets_and_headers_str,
       std::vector<std::string> extra_rs_sources,
       std::vector<std::string> srcs_to_scan_for_instantiations,
-      std::string instantiations_out);
+      std::string instantiations_out, std::string error_report_out);
 
   absl::StatusOr<BazelLabel> FindHeader(const HeaderName& header) const;
 
@@ -97,6 +98,7 @@
   std::string crubit_support_path_;
   std::string rustfmt_exe_path_;
   std::string rustfmt_config_path_;
+  std::string error_report_out_;
   bool do_nothing_ = true;
 
   BazelLabel current_target_;
diff --git a/rs_bindings_from_cc/cmdline_test.cc b/rs_bindings_from_cc/cmdline_test.cc
index c891f12..4bc7752 100644
--- a/rs_bindings_from_cc/cmdline_test.cc
+++ b/rs_bindings_from_cc/cmdline_test.cc
@@ -33,7 +33,8 @@
       std::move(targets_and_headers),
       /* extra_rs_srcs= */ {},
       /* srcs_to_scan_for_instantiations= */ {},
-      /*instantiations_out=*/"");
+      /* instantiations_out= */ "",
+      /* error_report_out= */ "");
 }
 
 }  // namespace
@@ -46,7 +47,8 @@
           "rustfmt_exe_path", "rustfmt_config_path",
           /* do_nothing= */ false, {"h1"},
           R"([{"t": "t1", "h": ["h1", "h2"]}])", {"extra_file.rs"},
-          {"scan_for_instantiations.rs"}, "instantiations_out"));
+          {"scan_for_instantiations.rs"}, "instantiations_out",
+          "error_report_out"));
   EXPECT_EQ(cmdline.cc_out(), "cc_out");
   EXPECT_EQ(cmdline.rs_out(), "rs_out");
   EXPECT_EQ(cmdline.ir_out(), "ir_out");
@@ -55,6 +57,7 @@
   EXPECT_EQ(cmdline.rustfmt_exe_path(), "rustfmt_exe_path");
   EXPECT_EQ(cmdline.rustfmt_config_path(), "rustfmt_config_path");
   EXPECT_EQ(cmdline.instantiations_out(), "instantiations_out");
+  EXPECT_EQ(cmdline.error_report_out(), "error_report_out");
   EXPECT_EQ(cmdline.do_nothing(), false);
   EXPECT_EQ(cmdline.current_target().value(), "t1");
   EXPECT_THAT(cmdline.public_headers(), ElementsAre(HeaderName("h1")));
@@ -205,7 +208,7 @@
           "rustfmt_exe_path", "rustfmt_config_path",
           /* do_nothing= */ false, {"a.h"}, std::string(kTargetsAndHeaders),
           /* extra_rs_srcs= */ {}, {"lib.rs"},
-          /* instantiations_out= */ "")),
+          /* instantiations_out= */ "", "error_report_out")),
       StatusIs(
           absl::StatusCode::kInvalidArgument,
           HasSubstr(
@@ -223,7 +226,8 @@
           "rustfmt_exe_path", "rustfmt_config_path",
           /* do_nothing= */ false, {"a.h"}, std::string(kTargetsAndHeaders),
           /* extra_rs_srcs= */ {},
-          /* srcs_to_scan_for_instantiations= */ {}, "instantiations_out"),
+          /* srcs_to_scan_for_instantiations= */ {}, "instantiations_out",
+          "error_report_out"),
       StatusIs(
           absl::StatusCode::kInvalidArgument,
           HasSubstr(
@@ -242,7 +246,7 @@
           /* do_nothing= */ false, {"a.h"}, std::string(kTargetsAndHeaders),
           /* extra_rs_srcs= */ {},
           /* srcs_to_scan_for_instantiations= */ {},
-          /* instantiations_out= */ ""),
+          /* instantiations_out= */ "", "error_report_out"),
       StatusIs(absl::StatusCode::kInvalidArgument,
                HasSubstr("please specify --cc_out")));
 }
@@ -258,7 +262,7 @@
           /* do_nothing= */ false, {"a.h"}, std::string(kTargetsAndHeaders),
           /* extra_rs_srcs= */ {},
           /* srcs_to_scan_for_instantiations= */ {},
-          /* instantiations_out= */ ""),
+          /* instantiations_out= */ "", "error_report_out"),
       StatusIs(absl::StatusCode::kInvalidArgument,
                HasSubstr("please specify --rs_out")));
 }
@@ -273,7 +277,7 @@
       /* do_nothing= */ false, {"a.h"}, std::string(kTargetsAndHeaders),
       /* extra_rs_srcs= */ {},
       /* srcs_to_scan_for_instantiations= */ {},
-      /* instantiations_out= */ ""));
+      /* instantiations_out= */ "", "error_report_out"));
 }
 
 TEST(CmdlineTest, RustfmtExePathEmpty) {
@@ -287,7 +291,7 @@
           /* do_nothing= */ false, {"a.h"}, std::string(kTargetsAndHeaders),
           /* extra_rs_srcs= */ {},
           /* srcs_to_scan_for_instantiations= */ {},
-          /* instantiations_out= */ ""),
+          /* instantiations_out= */ "", "error_report_out"),
       StatusIs(absl::StatusCode::kInvalidArgument,
                HasSubstr("please specify --rustfmt_exe_path")));
 }
diff --git a/rs_bindings_from_cc/error_report.rs b/rs_bindings_from_cc/error_report.rs
new file mode 100644
index 0000000..2386d32
--- /dev/null
+++ b/rs_bindings_from_cc/error_report.rs
@@ -0,0 +1,359 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+use std::borrow::Cow;
+use std::collections::BTreeMap;
+
+use serde::Serialize;
+
+#[doc(hidden)]
+pub mod macro_internal {
+    use std::borrow::Cow;
+    use std::fmt::{self, Arguments, Display, Formatter};
+
+    pub use anyhow;
+    pub use arc_anyhow;
+    pub use std::format_args;
+
+    /// An error that stores its format string as well as the formatted message.
+    #[derive(Debug, Clone)]
+    pub struct AttributedError {
+        pub fmt: Cow<'static, str>,
+        pub message: Cow<'static, str>,
+    }
+
+    impl AttributedError {
+        pub fn new_static(fmt: &'static str, args: Arguments) -> arc_anyhow::Error {
+            arc_anyhow::Error::from(anyhow::Error::from(match args.as_str() {
+                // This format string has no parameters.
+                Some(s) => Self { fmt: Cow::Borrowed(s), message: Cow::Borrowed(s) },
+                // This format string has parameters and must be formatted.
+                None => Self { fmt: Cow::Borrowed(fmt), message: Cow::Owned(fmt::format(args)) },
+            }))
+        }
+
+        pub fn new_dynamic(err: impl Display) -> arc_anyhow::Error {
+            // Use the whole error as the format string. This is preferable to
+            // grouping all dynamic errors under the "{}" format string.
+            let message = format!("{}", err);
+            arc_anyhow::Error::from(anyhow::Error::from(Self {
+                fmt: Cow::Owned(message.clone()),
+                message: Cow::Owned(message),
+            }))
+        }
+    }
+
+    impl Display for AttributedError {
+        fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+            write!(f, "{}", self.message)
+        }
+    }
+
+    impl std::error::Error for AttributedError {}
+}
+
+use crate::macro_internal::AttributedError;
+
+/// Evaluates to an [`arc_anyhow::Error`].
+///
+/// Otherwise similar to [`anyhow::anyhow`].
+#[macro_export]
+macro_rules! anyhow {
+    ($fmt:literal $(,)?) => {
+        $crate::macro_internal::AttributedError::new_static(
+            $fmt,
+            $crate::macro_internal::format_args!($fmt),
+        )
+    };
+    ($err:expr $(,)?) => {
+        $crate::macro_internal::AttributedError::new_dynamic($err)
+    };
+    ($fmt:expr, $($arg:tt)*) => {
+        $crate::macro_internal::AttributedError::new_static(
+            $fmt,
+            $crate::macro_internal::format_args!($fmt, $($arg)*),
+        )
+    };
+}
+
+/// Returns a [`Result::Err`] containing an [`arc_anyhow::Error`].
+///
+/// Otherwise similar to [`anyhow::bail`].
+#[macro_export]
+macro_rules! bail {
+    ($fmt:literal $(,)?) => {
+        return Err($crate::anyhow!($fmt))
+    };
+    ($err:expr $(,)?) => {
+        return Err($crate::anyhow!($err))
+    };
+    ($fmt:expr, $($arg:tt)*) => {
+        return Err($crate::anyhow!($fmt, $($arg)*))
+    };
+}
+
+/// Returns a [`Result::Err`] containing an [`arc_anyhow::Error`] if the given
+/// condition evaluates to false.
+///
+/// Otherwise similar to [`anyhow::ensure`].
+#[macro_export]
+macro_rules! ensure {
+    ($cond:expr, $fmt:literal $(,)?) => {
+        if !$cond { bail!($fmt); }
+    };
+    ($cond:expr, $err:expr $(,)?) => {
+        if !$cond { bail!($err); }
+    };
+    ($cond:expr, $fmt:expr, $($arg:tt)*) => {
+        if !$cond { bail!($fmt, $($arg)*); }
+    };
+}
+
+pub trait ErrorReporting {
+    fn insert(&mut self, error: &arc_anyhow::Error);
+    fn serialize_to_vec(&self) -> anyhow::Result<Vec<u8>>;
+}
+
+/// A null [`ErrorReporting`] strategy.
+pub struct IgnoreErrors;
+
+impl ErrorReporting for IgnoreErrors {
+    fn insert(&mut self, _error: &arc_anyhow::Error) {}
+
+    fn serialize_to_vec(&self) -> anyhow::Result<Vec<u8>> {
+        Ok(vec![])
+    }
+}
+
+/// An aggregate of zero or more errors.
+#[derive(Default, Serialize)]
+pub struct ErrorReport {
+    #[serde(flatten)]
+    map: BTreeMap<Cow<'static, str>, ErrorReportEntry>,
+}
+
+impl ErrorReport {
+    pub fn new() -> Self {
+        Self::default()
+    }
+}
+
+impl ErrorReporting for ErrorReport {
+    fn insert(&mut self, error: &arc_anyhow::Error) {
+        if let Some(error) = error.downcast_ref::<AttributedError>() {
+            let sample_message = if error.message != error.fmt { &*error.message } else { "" };
+            self.map.entry(error.fmt.clone()).or_default().add(Cow::Borrowed(sample_message));
+        } else {
+            self.map.entry(Cow::Borrowed("{}")).or_default().add(Cow::Owned(format!("{error}")));
+        }
+    }
+
+    fn serialize_to_vec(&self) -> anyhow::Result<Vec<u8>> {
+        Ok(serde_json::to_vec(self)?)
+    }
+}
+
+#[derive(Default, Serialize)]
+struct ErrorReportEntry {
+    count: u64,
+    #[serde(skip_serializing_if = "String::is_empty")]
+    sample_message: String,
+}
+
+impl ErrorReportEntry {
+    fn add(&mut self, message: Cow<str>) {
+        if self.count == 0 {
+            self.sample_message = message.into_owned();
+        }
+        self.count += 1;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::borrow::Cow;
+
+    #[test]
+    fn anyhow_1arg_static_plain() {
+        let arc_err = anyhow!("abc");
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Borrowed(_)));
+        assert_eq!(err.fmt, "abc");
+        assert!(matches!(err.message, Cow::Borrowed(_)));
+        assert_eq!(err.message, "abc");
+    }
+
+    #[test]
+    fn anyhow_1arg_static_fmt() {
+        let some_var = "def";
+        let arc_err = anyhow!("abc{some_var}");
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Borrowed(_)));
+        assert_eq!(err.fmt, "abc{some_var}");
+        assert!(matches!(err.message, Cow::Owned(_)));
+        assert_eq!(err.message, "abcdef");
+    }
+
+    #[test]
+    fn anyhow_1arg_dynamic() {
+        let arc_err = anyhow!(format!("abc{}", "def"));
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Owned(_)));
+        assert_eq!(err.fmt, "abcdef");
+        assert!(matches!(err.message, Cow::Owned(_)));
+        assert_eq!(err.message, "abcdef");
+    }
+
+    #[test]
+    fn anyhow_2arg() {
+        let arc_err = anyhow!("abc{}", "def");
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Borrowed(_)));
+        assert_eq!(err.fmt, "abc{}");
+        assert!(matches!(err.message, Cow::Owned(_)));
+        assert_eq!(err.message, "abcdef");
+    }
+
+    #[test]
+    fn bail_1arg_static_plain() {
+        let arc_err = (|| -> arc_anyhow::Result<()> { bail!("abc") })().unwrap_err();
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Borrowed(_)));
+        assert_eq!(err.fmt, "abc");
+        assert!(matches!(err.message, Cow::Borrowed(_)));
+        assert_eq!(err.message, "abc");
+    }
+
+    #[test]
+    fn bail_1arg_static_fmt() {
+        let some_var = "def";
+        let arc_err = (|| -> arc_anyhow::Result<()> { bail!("abc{some_var}") })().unwrap_err();
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Borrowed(_)));
+        assert_eq!(err.fmt, "abc{some_var}");
+        assert!(matches!(err.message, Cow::Owned(_)));
+        assert_eq!(err.message, "abcdef");
+    }
+
+    #[test]
+    fn bail_1arg_dynamic() {
+        let arc_err =
+            (|| -> arc_anyhow::Result<()> { bail!(format!("abc{}", "def")) })().unwrap_err();
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Owned(_)));
+        assert_eq!(err.fmt, "abcdef");
+        assert!(matches!(err.message, Cow::Owned(_)));
+        assert_eq!(err.message, "abcdef");
+    }
+
+    #[test]
+    fn bail_2arg() {
+        let arc_err = (|| -> arc_anyhow::Result<()> { bail!("abc{}", "def") })().unwrap_err();
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Borrowed(_)));
+        assert_eq!(err.fmt, "abc{}");
+        assert!(matches!(err.message, Cow::Owned(_)));
+        assert_eq!(err.message, "abcdef");
+    }
+
+    #[test]
+    fn ensure_pass() {
+        let f = || {
+            ensure!(true, "unused message");
+            Ok(())
+        };
+        f().unwrap();
+    }
+
+    #[test]
+    fn ensure_fail_1arg_static_plain() {
+        let arc_err = (|| {
+            ensure!(false, "abc");
+            Ok(())
+        })()
+        .unwrap_err();
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Borrowed(_)));
+        assert_eq!(err.fmt, "abc");
+        assert!(matches!(err.message, Cow::Borrowed(_)));
+        assert_eq!(err.message, "abc");
+    }
+
+    #[test]
+    fn ensure_fail_1arg_static_fmt() {
+        let some_var = "def";
+        let arc_err = (|| {
+            ensure!(false, "abc{some_var}");
+            Ok(())
+        })()
+        .unwrap_err();
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Borrowed(_)));
+        assert_eq!(err.fmt, "abc{some_var}");
+        assert!(matches!(err.message, Cow::Owned(_)));
+        assert_eq!(err.message, "abcdef");
+    }
+
+    #[test]
+    fn ensure_fail_1arg_dynamic() {
+        let arc_err = (|| {
+            ensure!(false, format!("abc{}", "def"));
+            Ok(())
+        })()
+        .unwrap_err();
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Owned(_)));
+        assert_eq!(err.fmt, "abcdef");
+        assert!(matches!(err.message, Cow::Owned(_)));
+        assert_eq!(err.message, "abcdef");
+    }
+
+    #[test]
+    fn ensure_fail_2arg() {
+        let arc_err = (|| {
+            ensure!(false, "abc{}", "def");
+            Ok(())
+        })()
+        .unwrap_err();
+        let err: &AttributedError = arc_err.downcast_ref().unwrap();
+        assert!(matches!(err.fmt, Cow::Borrowed(_)));
+        assert_eq!(err.fmt, "abc{}");
+        assert!(matches!(err.message, Cow::Owned(_)));
+        assert_eq!(err.message, "abcdef");
+    }
+
+    #[test]
+    fn error_report() {
+        let mut report = ErrorReport::new();
+        report.insert(&anyhow!("abc{}", "def"));
+        report.insert(&anyhow!("abc{}", "123"));
+        report.insert(&anyhow!("error code: {}", 65535));
+        report.insert(&anyhow!("no parameters"));
+        report.insert(&anyhow!("no parameters"));
+        report.insert(&anyhow!("no parameters"));
+        report.insert(&anyhow::Error::msg("not attributed").into());
+
+        assert_eq!(
+            serde_json::to_string_pretty(&report).unwrap(),
+            r#"{
+  "abc{}": {
+    "count": 2,
+    "sample_message": "abcdef"
+  },
+  "error code: {}": {
+    "count": 1,
+    "sample_message": "error code: 65535"
+  },
+  "no parameters": {
+    "count": 3
+  },
+  "{}": {
+    "count": 1,
+    "sample_message": "not attributed"
+  }
+}"#,
+        );
+    }
+}
diff --git a/rs_bindings_from_cc/generate_bindings_and_metadata.cc b/rs_bindings_from_cc/generate_bindings_and_metadata.cc
index 37d5a3a..7f0d126 100644
--- a/rs_bindings_from_cc/generate_bindings_and_metadata.cc
+++ b/rs_bindings_from_cc/generate_bindings_and_metadata.cc
@@ -73,10 +73,12 @@
                  cmdline.headers_to_targets(), cmdline.extra_rs_srcs(),
                  clang_args_view, requested_instantiations));
 
-  CRUBIT_ASSIGN_OR_RETURN(Bindings bindings,
-                          GenerateBindings(ir, cmdline.crubit_support_path(),
-                                           cmdline.rustfmt_exe_path(),
-                                           cmdline.rustfmt_config_path()));
+  bool generate_error_report = !cmdline.error_report_out().empty();
+  CRUBIT_ASSIGN_OR_RETURN(
+      Bindings bindings,
+      GenerateBindings(ir, cmdline.crubit_support_path(),
+                       cmdline.rustfmt_exe_path(),
+                       cmdline.rustfmt_config_path(), generate_error_report));
 
   absl::flat_hash_map<std::string, std::string> instantiations;
   std::optional<const Namespace*> ns =
@@ -97,6 +99,7 @@
       .rs_api_impl = bindings.rs_api_impl,
       .namespaces = std::move(top_level_namespaces),
       .instantiations = std::move(instantiations),
+      .error_report = bindings.error_report,
   };
 }
 
diff --git a/rs_bindings_from_cc/generate_bindings_and_metadata.h b/rs_bindings_from_cc/generate_bindings_and_metadata.h
index 2c56c83..6b30073 100644
--- a/rs_bindings_from_cc/generate_bindings_and_metadata.h
+++ b/rs_bindings_from_cc/generate_bindings_and_metadata.h
@@ -28,6 +28,8 @@
   // C++ class templates explicitly instantiated in this TU and their Rust
   // struct name.
   absl::flat_hash_map<std::string, std::string> instantiations;
+  // A JSON error report, if requested.
+  std::string error_report;
 };
 
 // Returns `BindingsAndMetadata` as requested by the user on the command line.
diff --git a/rs_bindings_from_cc/generate_bindings_and_metadata_test.cc b/rs_bindings_from_cc/generate_bindings_and_metadata_test.cc
index 6decefc..4cb60e3 100644
--- a/rs_bindings_from_cc/generate_bindings_and_metadata_test.cc
+++ b/rs_bindings_from_cc/generate_bindings_and_metadata_test.cc
@@ -42,7 +42,8 @@
           /* public_headers= */ {"a.h"}, std::string(kTargetsAndHeaders),
           /* extra_rs_srcs= */ {},
           /* srcs_to_scan_for_instantiations= */ {},
-          /* instantiations_out= */ ""));
+          /* instantiations_out= */ "",
+          /* error_report_out= */ ""));
 
   ASSERT_OK_AND_ASSIGN(
       BindingsAndMetadata result,
@@ -52,6 +53,7 @@
 
   ASSERT_EQ(result.ir.used_headers.size(), 1);
   ASSERT_EQ(result.ir.used_headers.front().IncludePath(), "a.h");
+  ASSERT_EQ(result.error_report, "");
 
   // Check that IR items have the proper owning target set.
   auto item = result.ir.get_items_if<Namespace>().front();
@@ -71,7 +73,8 @@
           /* public_headers= */ {"a.h"}, std::string(kTargetsAndHeaders),
           /* extra_rs_srcs= */ {},
           /* srcs_to_scan_for_instantiations= */ {},
-          /* instantiations_out= */ ""));
+          /* instantiations_out= */ "",
+          /* error_report_out= */ ""));
 
   ASSERT_OK_AND_ASSIGN(
       BindingsAndMetadata result,
@@ -100,7 +103,7 @@
           {"a.h"}, std::string(kTargetsAndHeaders),
           /* extra_rs_srcs= */ {},
           /* srcs_to_scan_for_instantiations= */ {a_rs_path},
-          "instantiations_out"));
+          "instantiations_out", /* error_report_out= */ ""));
 
   CRUBIT_ASSIGN_OR_RETURN(
       BindingsAndMetadata result,
@@ -267,7 +270,7 @@
           /* public_headers= */ {"a.h"}, std::string(kTargetsAndHeaders),
           /* extra_rs_srcs= */ {},
           /* srcs_to_scan_for_instantiations= */ {},
-          /* instantiations_out= */ ""));
+          /* instantiations_out= */ "", /* error_report_out= */ ""));
   ASSERT_OK_AND_ASSIGN(BindingsAndMetadata result,
                        GenerateBindingsAndMetadata(
                            cmdline, DefaultClangArgs(),
diff --git a/rs_bindings_from_cc/ir.rs b/rs_bindings_from_cc/ir.rs
index 3cb475a..4e6d2ba 100644
--- a/rs_bindings_from_cc/ir.rs
+++ b/rs_bindings_from_cc/ir.rs
@@ -6,13 +6,15 @@
 //! `rs_bindings_from_cc/ir.h` for more
 //! information.
 
-use arc_anyhow::{anyhow, bail, Context, Result};
+use arc_anyhow::{anyhow, bail, Context, Error, Result};
+use once_cell::unsync::OnceCell;
 use proc_macro2::{Literal, TokenStream};
 use quote::{quote, ToTokens, TokenStreamExt};
 use serde::Deserialize;
 use std::collections::hash_map::{Entry, HashMap};
 use std::convert::TryFrom;
-use std::fmt;
+use std::fmt::{self, Debug, Formatter};
+use std::hash::{Hash, Hasher};
 use std::io::Read;
 use std::rc::Rc;
 
@@ -499,12 +501,60 @@
     pub column: u64,
 }
 
+/// A wrapper type that does not contribute to equality or hashing. All
+/// instances are equal.
+#[derive(Clone, Copy, Default)]
+struct IgnoredField<T>(T);
+
+impl<T> Debug for IgnoredField<T> {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        write!(f, "_")
+    }
+}
+
+impl<T> PartialEq for IgnoredField<T> {
+    fn eq(&self, _other: &Self) -> bool {
+        true
+    }
+}
+
+impl<T> Eq for IgnoredField<T> {}
+
+impl<T> Hash for IgnoredField<T> {
+    fn hash<H: Hasher>(&self, _state: &mut H) {}
+}
+
 #[derive(Debug, PartialEq, Eq, Hash, Clone, Deserialize)]
 pub struct UnsupportedItem {
     pub name: String,
-    pub message: String,
+    message: String,
     pub source_loc: SourceLoc,
     pub id: ItemId,
+    #[serde(skip)]
+    cause: IgnoredField<OnceCell<Error>>,
+}
+
+impl UnsupportedItem {
+    pub fn new_with_message(
+        name: String,
+        message: String,
+        source_loc: SourceLoc,
+        id: ItemId,
+    ) -> Self {
+        Self { name, message, source_loc, id, cause: Default::default() }
+    }
+
+    pub fn new_with_cause(name: String, cause: Error, source_loc: SourceLoc, id: ItemId) -> Self {
+        Self { name, message: cause.to_string(), source_loc, id, cause: IgnoredField(cause.into()) }
+    }
+
+    pub fn message(&self) -> &str {
+        &self.message
+    }
+
+    pub fn cause(&self) -> &Error {
+        self.cause.0.get_or_init(|| anyhow!(self.message.clone()))
+    }
 }
 
 #[derive(Debug, PartialEq, Eq, Hash, Clone, Deserialize)]
@@ -581,7 +631,7 @@
 }
 
 impl<'a> TryFrom<&'a Item> for &'a Rc<Func> {
-    type Error = arc_anyhow::Error;
+    type Error = Error;
     fn try_from(value: &'a Item) -> Result<Self, Self::Error> {
         if let Item::Func(f) = value { Ok(f) } else { bail!("Not a Func: {:#?}", value) }
     }
@@ -594,7 +644,7 @@
 }
 
 impl<'a> TryFrom<&'a Item> for &'a Rc<Record> {
-    type Error = arc_anyhow::Error;
+    type Error = Error;
     fn try_from(value: &'a Item) -> Result<Self, Self::Error> {
         if let Item::Record(r) = value { Ok(r) } else { bail!("Not a Record: {:#?}", value) }
     }
@@ -607,7 +657,7 @@
 }
 
 impl<'a> TryFrom<&'a Item> for &'a Rc<UnsupportedItem> {
-    type Error = arc_anyhow::Error;
+    type Error = Error;
     fn try_from(value: &'a Item) -> Result<Self, Self::Error> {
         if let Item::UnsupportedItem(u) = value {
             Ok(u)
@@ -624,7 +674,7 @@
 }
 
 impl<'a> TryFrom<&'a Item> for &'a Rc<Comment> {
-    type Error = arc_anyhow::Error;
+    type Error = Error;
     fn try_from(value: &'a Item) -> Result<Self, Self::Error> {
         if let Item::Comment(c) = value { Ok(c) } else { bail!("Not a Comment: {:#?}", value) }
     }
@@ -709,7 +759,7 @@
 
     pub fn item_for_type<T>(&self, ty: &T) -> Result<&Item>
     where
-        T: TypeWithDeclId + std::fmt::Debug,
+        T: TypeWithDeclId + Debug,
     {
         if let Some(decl_id) = ty.decl_id() {
             self.find_untyped_decl(decl_id)
diff --git a/rs_bindings_from_cc/ir_from_cc_test.rs b/rs_bindings_from_cc/ir_from_cc_test.rs
index 1c2a97d..521ada9 100644
--- a/rs_bindings_from_cc/ir_from_cc_test.rs
+++ b/rs_bindings_from_cc/ir_from_cc_test.rs
@@ -2903,7 +2903,7 @@
 #[test]
 fn test_volatile_is_unsupported() {
     let ir = ir_from_cc("volatile int* foo();").unwrap();
-    let f = ir.unsupported_items().find(|i| i.message.contains("volatile")).unwrap();
+    let f = ir.unsupported_items().find(|i| i.message().contains("volatile")).unwrap();
     assert_eq!("foo", f.name);
 }
 
diff --git a/rs_bindings_from_cc/rs_bindings_from_cc.cc b/rs_bindings_from_cc/rs_bindings_from_cc.cc
index dabf607..b6fd721 100644
--- a/rs_bindings_from_cc/rs_bindings_from_cc.cc
+++ b/rs_bindings_from_cc/rs_bindings_from_cc.cc
@@ -82,6 +82,11 @@
         crubit::NamespacesAsJson(bindings_and_metadata.namespaces)));
   }
 
+  if (!cmdline.error_report_out().empty()) {
+    CRUBIT_RETURN_IF_ERROR(SetFileContents(cmdline.error_report_out(),
+                                           bindings_and_metadata.error_report));
+  }
+
   return absl::OkStatus();
 }
 
diff --git a/rs_bindings_from_cc/src_code_gen.cc b/rs_bindings_from_cc/src_code_gen.cc
index 9ead1e9..a4f23e5 100644
--- a/rs_bindings_from_cc/src_code_gen.cc
+++ b/rs_bindings_from_cc/src_code_gen.cc
@@ -18,13 +18,15 @@
 struct FfiBindings {
   FfiU8SliceBox rs_api;
   FfiU8SliceBox rs_api_impl;
+  FfiU8SliceBox error_report;
 };
 
 // This function is implemented in Rust.
 extern "C" FfiBindings GenerateBindingsImpl(FfiU8Slice json,
                                             FfiU8Slice crubit_support_path,
                                             FfiU8Slice rustfmt_exe_path,
-                                            FfiU8Slice rustfmt_config_path);
+                                            FfiU8Slice rustfmt_config_path,
+                                            bool generate_error_report);
 
 // Creates `Bindings` instance from copied data from `ffi_bindings`.
 static absl::StatusOr<Bindings> MakeBindingsFromFfiBindings(
@@ -33,9 +35,11 @@
 
   const FfiU8SliceBox& rs_api = ffi_bindings.rs_api;
   const FfiU8SliceBox& rs_api_impl = ffi_bindings.rs_api_impl;
+  const FfiU8SliceBox& error_report = ffi_bindings.error_report;
 
   bindings.rs_api = std::string(rs_api.ptr, rs_api.size);
   bindings.rs_api_impl = std::string(rs_api_impl.ptr, rs_api_impl.size);
+  bindings.error_report = std::string(error_report.ptr, error_report.size);
   return bindings;
 }
 
@@ -43,16 +47,20 @@
 static void FreeFfiBindings(FfiBindings ffi_bindings) {
   FreeFfiU8SliceBox(ffi_bindings.rs_api);
   FreeFfiU8SliceBox(ffi_bindings.rs_api_impl);
+  FreeFfiU8SliceBox(ffi_bindings.error_report);
 }
 
-absl::StatusOr<Bindings> GenerateBindings(
-    const IR& ir, absl::string_view crubit_support_path,
-    absl::string_view rustfmt_exe_path, absl::string_view rustfmt_config_path) {
+absl::StatusOr<Bindings> GenerateBindings(const IR& ir,
+                                          absl::string_view crubit_support_path,
+                                          absl::string_view rustfmt_exe_path,
+                                          absl::string_view rustfmt_config_path,
+                                          bool generate_error_report) {
   std::string json = llvm::formatv("{0}", ir.ToJson());
 
   FfiBindings ffi_bindings = GenerateBindingsImpl(
       MakeFfiU8Slice(json), MakeFfiU8Slice(crubit_support_path),
-      MakeFfiU8Slice(rustfmt_exe_path), MakeFfiU8Slice(rustfmt_config_path));
+      MakeFfiU8Slice(rustfmt_exe_path), MakeFfiU8Slice(rustfmt_config_path),
+      generate_error_report);
   CRUBIT_ASSIGN_OR_RETURN(Bindings bindings,
                           MakeBindingsFromFfiBindings(ffi_bindings));
   FreeFfiBindings(ffi_bindings);
diff --git a/rs_bindings_from_cc/src_code_gen.h b/rs_bindings_from_cc/src_code_gen.h
index 6382eea..ee77113 100644
--- a/rs_bindings_from_cc/src_code_gen.h
+++ b/rs_bindings_from_cc/src_code_gen.h
@@ -19,12 +19,16 @@
   std::string rs_api;
   // C++ source code.
   std::string rs_api_impl;
+  // Optional JSON error report.
+  std::string error_report;
 };
 
 // Generates bindings from the given `IR`.
-absl::StatusOr<Bindings> GenerateBindings(
-    const IR& ir, absl::string_view crubit_support_path,
-    absl::string_view rustfmt_exe_path, absl::string_view rustfmt_config_path);
+absl::StatusOr<Bindings> GenerateBindings(const IR& ir,
+                                          absl::string_view crubit_support_path,
+                                          absl::string_view rustfmt_exe_path,
+                                          absl::string_view rustfmt_config_path,
+                                          bool generate_error_report);
 
 }  // namespace crubit
 
diff --git a/rs_bindings_from_cc/src_code_gen.rs b/rs_bindings_from_cc/src_code_gen.rs
index 3982cc9..caf41ac 100644
--- a/rs_bindings_from_cc/src_code_gen.rs
+++ b/rs_bindings_from_cc/src_code_gen.rs
@@ -2,7 +2,8 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
-use arc_anyhow::{anyhow, bail, ensure, Context, Result};
+use arc_anyhow::{Context, Result};
+use error_report::{anyhow, bail, ensure, ErrorReport, ErrorReporting, IgnoreErrors};
 use ffi_types::*;
 use ir::*;
 use itertools::Itertools;
@@ -26,6 +27,7 @@
 pub struct FfiBindings {
     rs_api: FfiU8SliceBox,
     rs_api_impl: FfiU8SliceBox,
+    error_report: FfiU8SliceBox,
 }
 
 /// Deserializes IR from `json` and generates bindings source code.
@@ -57,6 +59,7 @@
     crubit_support_path: FfiU8Slice,
     rustfmt_exe_path: FfiU8Slice,
     rustfmt_config_path: FfiU8Slice,
+    generate_error_report: bool,
 ) -> FfiBindings {
     let json: &[u8] = json.as_slice();
     let crubit_support_path: &str = std::str::from_utf8(crubit_support_path.as_slice()).unwrap();
@@ -66,14 +69,31 @@
         std::str::from_utf8(rustfmt_config_path.as_slice()).unwrap().into();
     catch_unwind(|| {
         // It is ok to abort here.
-        let Bindings { rs_api, rs_api_impl } =
-            generate_bindings(json, crubit_support_path, &rustfmt_exe_path, &rustfmt_config_path)
-                .unwrap();
+        let mut error_report;
+        let mut ignore_errors;
+        let errors: &mut dyn ErrorReporting = if generate_error_report {
+            error_report = ErrorReport::new();
+            &mut error_report
+        } else {
+            ignore_errors = IgnoreErrors;
+            &mut ignore_errors
+        };
+        let Bindings { rs_api, rs_api_impl } = generate_bindings(
+            json,
+            crubit_support_path,
+            &rustfmt_exe_path,
+            &rustfmt_config_path,
+            errors,
+        )
+        .unwrap();
         FfiBindings {
             rs_api: FfiU8SliceBox::from_boxed_slice(rs_api.into_bytes().into_boxed_slice()),
             rs_api_impl: FfiU8SliceBox::from_boxed_slice(
                 rs_api_impl.into_bytes().into_boxed_slice(),
             ),
+            error_report: FfiU8SliceBox::from_boxed_slice(
+                errors.serialize_to_vec().unwrap().into_boxed_slice(),
+            ),
         }
     })
     .unwrap_or_else(|_| process::abort())
@@ -126,11 +146,12 @@
     crubit_support_path: &str,
     rustfmt_exe_path: &OsStr,
     rustfmt_config_path: &OsStr,
+    errors: &mut dyn ErrorReporting,
 ) -> Result<Bindings> {
     let ir = Rc::new(deserialize_ir(json)?);
 
     let BindingsTokens { rs_api, rs_api_impl } =
-        generate_bindings_tokens(ir.clone(), crubit_support_path)?;
+        generate_bindings_tokens(ir.clone(), crubit_support_path, errors)?;
     let rs_api = {
         let rustfmt_config = RustfmtConfig::new(rustfmt_exe_path, rustfmt_config_path);
         rs_tokens_to_formatted_string(rs_api, &rustfmt_config)?
@@ -348,22 +369,22 @@
 }
 
 fn make_unsupported_fn(func: &Func, ir: &IR, message: impl ToString) -> Result<UnsupportedItem> {
-    Ok(UnsupportedItem {
-        name: cxx_function_name(func, ir)?,
-        message: message.to_string(),
-        source_loc: func.source_loc.clone(),
-        id: func.id,
-    })
+    Ok(UnsupportedItem::new_with_message(
+        cxx_function_name(func, ir)?,
+        message.to_string(),
+        func.source_loc.clone(),
+        func.id,
+    ))
 }
 
 fn make_unsupported_nested_type_alias(type_alias: &TypeAlias) -> Result<UnsupportedItem> {
-    Ok(UnsupportedItem {
+    Ok(UnsupportedItem::new_with_message(
         // TODO(jeanpierreda): It would be nice to include the enclosing record name here too.
-        name: type_alias.identifier.identifier.to_string(),
-        message: "Typedefs nested in classes are not supported yet".to_string(),
-        source_loc: type_alias.source_loc.clone(),
-        id: type_alias.id,
-    })
+        type_alias.identifier.identifier.to_string(),
+        "Typedefs nested in classes are not supported yet".to_string(),
+        type_alias.source_loc.clone(),
+        type_alias.id,
+    ))
 }
 
 /// The name of a one-function trait, with extra entries for
@@ -1249,7 +1270,11 @@
             *trait_generic_params = lifetimes
                 .iter()
                 .filter_map(|lifetime| {
-                    if trait_lifetimes.contains(lifetime) { Some(quote! {#lifetime}) } else { None }
+                    if trait_lifetimes.contains(lifetime) {
+                        Some(quote! {#lifetime})
+                    } else {
+                        None
+                    }
                 })
                 .collect();
         } else {
@@ -1789,7 +1814,11 @@
 
 /// Generates Rust source code for a given `Record` and associated assertions as
 /// a tuple.
-fn generate_record(db: &Database, record: &Rc<Record>) -> Result<GeneratedItem> {
+fn generate_record(
+    db: &Database,
+    record: &Rc<Record>,
+    errors: &mut dyn ErrorReporting,
+) -> Result<GeneratedItem> {
     let ir = db.ir();
     let ident = make_rs_ident(&record.rs_name);
     let namespace_qualifier = NamespaceQualifier::new(record.id, &ir)?.format_for_rs();
@@ -2075,7 +2104,7 @@
             let item = ir.find_decl(*id).with_context(|| {
                 format!("Failed to look up `record.child_item_ids` for {:?}", record)
             })?;
-            generate_item(db, item)
+            generate_item(db, item, errors)
         })
         .collect::<Result<Vec<_>>>()?;
 
@@ -2249,7 +2278,12 @@
 }
 
 /// Generates Rust source code for a given `UnsupportedItem`.
-fn generate_unsupported(item: &UnsupportedItem) -> Result<TokenStream> {
+fn generate_unsupported(
+    item: &UnsupportedItem,
+    errors: &mut dyn ErrorReporting,
+) -> Result<TokenStream> {
+    errors.insert(item.cause());
+
     let location = if item.source_loc.filename.is_empty() {
         "<unknown location>".to_string()
     } else {
@@ -2261,7 +2295,9 @@
     };
     let message = format!(
         "{}\nError while generating bindings for item '{}':\n{}",
-        &location, &item.name, &item.message
+        &location,
+        &item.name,
+        item.message()
     );
     Ok(quote! { __COMMENT__ #message })
 }
@@ -2272,7 +2308,11 @@
     Ok(quote! { __COMMENT__ #text })
 }
 
-fn generate_namespace(db: &Database, namespace: &Namespace) -> Result<GeneratedItem> {
+fn generate_namespace(
+    db: &Database,
+    namespace: &Namespace,
+    errors: &mut dyn ErrorReporting,
+) -> Result<GeneratedItem> {
     let ir = db.ir();
     let mut items = vec![];
     let mut thunks = vec![];
@@ -2285,7 +2325,7 @@
         let item = ir.find_decl(*item_id).with_context(|| {
             format!("Failed to look up namespace.child_item_ids for {:?}", namespace)
         })?;
-        let generated = generate_item(db, item)?;
+        let generated = generate_item(db, item, errors)?;
         items.push(generated.item);
         if !generated.thunks.is_empty() {
             thunks.push(generated.thunks);
@@ -2361,13 +2401,20 @@
     has_record: bool,
 }
 
-fn generate_item(db: &Database, item: &Item) -> Result<GeneratedItem> {
+fn generate_item(
+    db: &Database,
+    item: &Item,
+    errors: &mut dyn ErrorReporting,
+) -> Result<GeneratedItem> {
     let ir = db.ir();
     let overloaded_funcs = db.overloaded_funcs();
     let generated_item = match item {
         Item::Func(func) => match db.generate_func(func.clone()) {
             Err(e) => GeneratedItem {
-                item: generate_unsupported(&make_unsupported_fn(func, &ir, format!("{e}"))?)?,
+                item: generate_unsupported(
+                    &make_unsupported_fn(func, &ir, format!("{e}"))?,
+                    errors,
+                )?,
                 ..Default::default()
             },
             Ok(None) => GeneratedItem::default(),
@@ -2375,11 +2422,14 @@
                 let (api_func, thunk, function_id) = &*f;
                 if overloaded_funcs.contains(function_id) {
                     GeneratedItem {
-                        item: generate_unsupported(&make_unsupported_fn(
-                            func,
-                            &ir,
-                            "Cannot generate bindings for overloaded function",
-                        )?)?,
+                        item: generate_unsupported(
+                            &make_unsupported_fn(
+                                func,
+                                &ir,
+                                "Cannot generate bindings for overloaded function",
+                            )?,
+                            errors,
+                        )?,
                         ..Default::default()
                     }
                 } else {
@@ -2412,7 +2462,7 @@
             {
                 GeneratedItem::default()
             } else {
-                generate_record(db, record)?
+                generate_record(db, record, errors)?
             }
         }
         Item::Enum(enum_) => {
@@ -2432,7 +2482,10 @@
             } else if type_alias.enclosing_record_id.is_some() {
                 // TODO(b/200067824): support nested type aliases.
                 GeneratedItem {
-                    item: generate_unsupported(&make_unsupported_nested_type_alias(type_alias)?)?,
+                    item: generate_unsupported(
+                        &make_unsupported_nested_type_alias(type_alias)?,
+                        errors,
+                    )?,
                     ..Default::default()
                 }
             } else {
@@ -2440,12 +2493,12 @@
             }
         }
         Item::UnsupportedItem(unsupported) => {
-            GeneratedItem { item: generate_unsupported(unsupported)?, ..Default::default() }
+            GeneratedItem { item: generate_unsupported(unsupported, errors)?, ..Default::default() }
         }
         Item::Comment(comment) => {
             GeneratedItem { item: generate_comment(comment)?, ..Default::default() }
         }
-        Item::Namespace(namespace) => generate_namespace(db, namespace)?,
+        Item::Namespace(namespace) => generate_namespace(db, namespace, errors)?,
         Item::UseMod(use_mod) => {
             let UseMod { path, mod_name, .. } = &**use_mod;
             let mod_name = make_rs_ident(&mod_name.identifier);
@@ -2482,7 +2535,11 @@
 
 // Returns the Rust code implementing bindings, plus any auxiliary C++ code
 // needed to support it.
-fn generate_bindings_tokens(ir: Rc<IR>, crubit_support_path: &str) -> Result<BindingsTokens> {
+fn generate_bindings_tokens(
+    ir: Rc<IR>,
+    crubit_support_path: &str,
+    errors: &mut dyn ErrorReporting,
+) -> Result<BindingsTokens> {
     let mut db = Database::default();
     db.set_ir(ir.clone());
 
@@ -2511,7 +2568,7 @@
     for top_level_item_id in ir.top_level_item_ids() {
         let item =
             ir.find_decl(*top_level_item_id).context("Failed to look up ir.top_level_item_ids")?;
-        let generated = generate_item(&db, item)?;
+        let generated = generate_item(&db, item, errors)?;
         items.push(generated.item);
         if !generated.thunks.is_empty() {
             thunks.push(generated.thunks);
@@ -3668,7 +3725,7 @@
     use token_stream_printer::rs_tokens_to_formatted_string_for_tests;
 
     fn generate_bindings_tokens(ir: Rc<IR>) -> Result<BindingsTokens> {
-        super::generate_bindings_tokens(ir, "crubit/rs_bindings_support")
+        super::generate_bindings_tokens(ir, "crubit/rs_bindings_support", &mut IgnoreErrors)
     }
 
     fn db_from_cc(cc_src: &str) -> Result<Database> {