diff --git a/content/browser/devtools/devtools_instrumentation.cc b/content/browser/devtools/devtools_instrumentation.cc
index ba619ff51dbbc..d7e9033daa96f 100644
--- a/content/browser/devtools/devtools_instrumentation.cc
+++ b/content/browser/devtools/devtools_instrumentation.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 #include "content/browser/devtools/devtools_instrumentation.h"
 
+#include "base/strings/strcat.h"
 #include "base/strings/stringprintf.h"
 #include "components/download/public/common/download_create_info.h"
 #include "components/download/public/common/download_item.h"
@@ -982,6 +983,21 @@ void OnWebTransportHandshakeFailed(
   DispatchToAgents(ftn, &protocol::LogHandler::EntryAdded, entry.get());
 }
 
+void LogWorkletError(RenderFrameHostImpl* frame_host,
+                     const std::string& error) {
+  FrameTreeNode* ftn = frame_host->frame_tree_node();
+  if (!ftn)
+    return;
+  std::string text = base::StrCat({"Worklet error: ", error});
+  auto entry = protocol::Log::LogEntry::Create()
+                   .SetSource(protocol::Log::LogEntry::SourceEnum::Other)
+                   .SetLevel(protocol::Log::LogEntry::LevelEnum::Error)
+                   .SetText(text)
+                   .SetTimestamp(base::Time::Now().ToDoubleT() * 1000.0)
+                   .Build();
+  DispatchToAgents(ftn, &protocol::LogHandler::EntryAdded, entry.get());
+}
+
 void ApplyNetworkContextParamsOverrides(
     BrowserContext* browser_context,
     network::mojom::NetworkContextParams* context_params) {
diff --git a/content/browser/devtools/devtools_instrumentation.h b/content/browser/devtools/devtools_instrumentation.h
index dad7c6dd8142a..32ffbe01a07d4 100644
--- a/content/browser/devtools/devtools_instrumentation.h
+++ b/content/browser/devtools/devtools_instrumentation.h
@@ -211,6 +211,9 @@ void OnWebTransportHandshakeFailed(
     const GURL& url,
     const base::Optional<net::QuicTransportError>& error);
 
+// Adds a debug error message from a worklet to the devtools console.
+void LogWorkletError(RenderFrameHostImpl* frame_host, const std::string& error);
+
 void ApplyNetworkContextParamsOverrides(
     BrowserContext* browser_context,
     network::mojom::NetworkContextParams* network_context_params);
diff --git a/content/browser/interest_group/ad_auction_service_impl.cc b/content/browser/interest_group/ad_auction_service_impl.cc
index 1a0d6fcbd1d94..6b64c3e7b3dda 100644
--- a/content/browser/interest_group/ad_auction_service_impl.cc
+++ b/content/browser/interest_group/ad_auction_service_impl.cc
@@ -13,6 +13,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/optional.h"
 #include "base/strings/stringprintf.h"
+#include "content/browser/devtools/devtools_instrumentation.h"
 #include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/service_sandbox_type.h"
 #include "content/browser/storage_partition_impl.h"
@@ -322,10 +323,17 @@ void AdAuctionServiceImpl::WorkletComplete(
     const url::Origin& owner,
     const std::string& name,
     auction_worklet::mojom::WinningBidderReportPtr bidder_report,
-    auction_worklet::mojom::SellerReportPtr seller_report) {
+    auction_worklet::mojom::SellerReportPtr seller_report,
+    const std::vector<std::string>& errors) {
   // Release process if needed.
   AuctionComplete();
 
+  // Forward debug information to devtools.
+  for (const std::string& error : errors) {
+    devtools_instrumentation::LogWorkletError(
+        static_cast<RenderFrameHostImpl*>(render_frame_host()), error);
+  }
+
   // Check if returned winner's information is valid.
   ValidatedResult result =
       ValidateAuctionResult(bidders, render_url, owner, name);
diff --git a/content/browser/interest_group/ad_auction_service_impl.h b/content/browser/interest_group/ad_auction_service_impl.h
index 2bf799c1ab72f..7e26a5d70bda9 100644
--- a/content/browser/interest_group/ad_auction_service_impl.h
+++ b/content/browser/interest_group/ad_auction_service_impl.h
@@ -94,7 +94,8 @@ class CONTENT_EXPORT AdAuctionServiceImpl final
       const url::Origin& owner,
       const std::string& name,
       auction_worklet::mojom::WinningBidderReportPtr bidder_report,
-      auction_worklet::mojom::SellerReportPtr seller_report);
+      auction_worklet::mojom::SellerReportPtr seller_report,
+      const std::vector<std::string>& errors);
 
   // Returns an untrusted URLLoaderFactory created by the RenderFrameHost,
   // suitable for loading URLs like subresources. Caches the factory in
diff --git a/content/services/auction_worklet/auction_downloader.cc b/content/services/auction_worklet/auction_downloader.cc
index 7a721156c25ab..01ace1e677886 100644
--- a/content/services/auction_worklet/auction_downloader.cc
+++ b/content/services/auction_worklet/auction_downloader.cc
@@ -11,7 +11,10 @@
 #include "base/bind.h"
 #include "base/callback.h"
 #include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "net/base/net_errors.h"
 #include "net/http/http_request_headers.h"
+#include "net/http/http_status_code.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
 #include "net/url_request/redirect_info.h"
 #include "services/network/public/cpp/resource_request.h"
@@ -87,7 +90,8 @@ AuctionDownloader::AuctionDownloader(
     const GURL& source_url,
     MimeType mime_type,
     AuctionDownloaderCallback auction_downloader_callback)
-    : mime_type_(mime_type),
+    : source_url_(source_url),
+      mime_type_(mime_type),
       auction_downloader_callback_(std::move(auction_downloader_callback)) {
   DCHECK(auction_downloader_callback_);
   auto resource_request = std::make_unique<network::ResourceRequest>();
@@ -100,8 +104,6 @@ AuctionDownloader::AuctionDownloader(
   simple_url_loader_ = network::SimpleURLLoader::Create(
       std::move(resource_request), kTrafficAnnotation);
 
-  // TODO(mmenke): Reject unexpected charsets.
-
   // Abort on redirects.
   // TODO(mmenke): May want a browser-side proxy to block redirects instead.
   simple_url_loader_->SetOnRedirectCallback(base::BindRepeating(
@@ -120,19 +122,54 @@ void AuctionDownloader::OnBodyReceived(std::unique_ptr<std::string> body) {
 
   auto simple_url_loader = std::move(simple_url_loader_);
   std::string allow_fledge;
-  if (!body || !simple_url_loader->ResponseInfo()->headers ||
-      !simple_url_loader->ResponseInfo()->headers->GetNormalizedHeader(
-          "X-Allow-FLEDGE", &allow_fledge) ||
-      !base::EqualsCaseInsensitiveASCII(allow_fledge, "true") ||
-      // Note that ResponseInfo's `mime_type` is always lowercase.
-      !MimeTypeIsConsistent(mime_type_,
-                            simple_url_loader->ResponseInfo()->mime_type) ||
-      !IsAllowedCharset(simple_url_loader->ResponseInfo()->charset, *body)) {
-    std::move(auction_downloader_callback_).Run(nullptr);
-    return;
-  }
 
-  std::move(auction_downloader_callback_).Run(std::move(body));
+  if (!body) {
+    std::string error_msg;
+    if (simple_url_loader->ResponseInfo() &&
+        simple_url_loader->ResponseInfo()->headers &&
+        simple_url_loader->ResponseInfo()->headers->response_code() / 100 !=
+            2) {
+      int status = simple_url_loader->ResponseInfo()->headers->response_code();
+      error_msg = base::StringPrintf(
+          "Failed to load %s HTTP status = %d %s.", source_url_.spec().c_str(),
+          status,
+          simple_url_loader->ResponseInfo()->headers->GetStatusText().c_str());
+    } else {
+      error_msg = base::StringPrintf(
+          "Failed to load %s error = %s.", source_url_.spec().c_str(),
+          net::ErrorToString(simple_url_loader->NetError()).c_str());
+    }
+    std::move(auction_downloader_callback_).Run(nullptr /* body */, error_msg);
+  } else if (!simple_url_loader->ResponseInfo()->headers ||
+             !simple_url_loader->ResponseInfo()->headers->GetNormalizedHeader(
+                 "X-Allow-FLEDGE", &allow_fledge) ||
+             !base::EqualsCaseInsensitiveASCII(allow_fledge, "true")) {
+    std::move(auction_downloader_callback_)
+        .Run(nullptr /* body */,
+             base::StringPrintf(
+                 "Rejecting load of %s due to lack of X-Allow-FLEDGE: true.",
+                 source_url_.spec().c_str()));
+  } else if (!MimeTypeIsConsistent(
+                 mime_type_,
+                 // ResponseInfo's `mime_type` is always lowercase.
+                 simple_url_loader->ResponseInfo()->mime_type)) {
+    std::move(auction_downloader_callback_)
+        .Run(nullptr /* body */,
+             base::StringPrintf(
+                 "Rejecting load of %s due to unexpected MIME type.",
+                 source_url_.spec().c_str()));
+  } else if (!IsAllowedCharset(simple_url_loader->ResponseInfo()->charset,
+                               *body)) {
+    std::move(auction_downloader_callback_)
+        .Run(nullptr /* body */,
+             base::StringPrintf(
+                 "Rejecting load of %s due to unexpected charset.",
+                 source_url_.spec().c_str()));
+  } else {
+    // All OK!
+    std::move(auction_downloader_callback_)
+        .Run(std::move(body), base::nullopt /* error_msg */);
+  }
 }
 
 void AuctionDownloader::OnRedirect(
@@ -144,7 +181,9 @@ void AuctionDownloader::OnRedirect(
   // Need to cancel the load, to prevent the request from continuing.
   simple_url_loader_.reset();
 
-  std::move(auction_downloader_callback_).Run(nullptr /* body */);
+  std::move(auction_downloader_callback_)
+      .Run(nullptr /* body */, base::StringPrintf("Unexpected redirect on %s.",
+                                                  source_url_.spec().c_str()));
 }
 
 }  // namespace auction_worklet
diff --git a/content/services/auction_worklet/auction_downloader.h b/content/services/auction_worklet/auction_downloader.h
index 0ef694ed67fce..55c98737ae884 100644
--- a/content/services/auction_worklet/auction_downloader.h
+++ b/content/services/auction_worklet/auction_downloader.h
@@ -9,6 +9,7 @@
 #include <string>
 
 #include "base/callback.h"
+#include "base/optional.h"
 #include "net/url_request/redirect_info.h"
 #include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
@@ -32,11 +33,9 @@ class AuctionDownloader {
   };
 
   // Passes in nullptr on failure. Always invoked asynchronously.
-  //
-  // TODO(mmenke): Pass along some sort of error string on failure, to make
-  // debugging easier.
   using AuctionDownloaderCallback =
-      base::OnceCallback<void(std::unique_ptr<std::string> response_body)>;
+      base::OnceCallback<void(std::unique_ptr<std::string> response_body,
+                              base::Optional<std::string> error)>;
 
   // Starts loading the worklet script on construction. Callback will be invoked
   // asynchronously once the data has been fetched or an error has occurred.
@@ -55,6 +54,7 @@ class AuctionDownloader {
                   const network::mojom::URLResponseHead& response_head,
                   std::vector<std::string>* removed_headers);
 
+  const GURL source_url_;
   const MimeType mime_type_;
   std::unique_ptr<network::SimpleURLLoader> simple_url_loader_;
   AuctionDownloaderCallback auction_downloader_callback_;
diff --git a/content/services/auction_worklet/auction_downloader_unittest.cc b/content/services/auction_worklet/auction_downloader_unittest.cc
index 02fd7f0235178..6abf337105f9d 100644
--- a/content/services/auction_worklet/auction_downloader_unittest.cc
+++ b/content/services/auction_worklet/auction_downloader_unittest.cc
@@ -52,23 +52,32 @@ class AuctionDownloaderTest : public testing::Test {
     return std::move(body_);
   }
 
+  // Helper to avoid checking has_value all over the place.
+  std::string last_error_msg() const {
+    return error_.value_or("Not an error.");
+  }
+
  protected:
-  void DownloadCompleteCallback(std::unique_ptr<std::string> body) {
+  void DownloadCompleteCallback(std::unique_ptr<std::string> body,
+                                base::Optional<std::string> error) {
     DCHECK(!body_);
     DCHECK(run_loop_);
     body_ = std::move(body);
+    error_ = std::move(error);
+    EXPECT_EQ(error_.has_value(), !body_);
     run_loop_->Quit();
   }
 
   base::test::TaskEnvironment task_environment_;
 
-  const GURL url_ = GURL("https://url.test/");
+  const GURL url_ = GURL("https://url.test/script.js");
 
   AuctionDownloader::MimeType mime_type_ =
       AuctionDownloader::MimeType::kJavascript;
 
   std::unique_ptr<base::RunLoop> run_loop_;
   std::unique_ptr<std::string> body_;
+  base::Optional<std::string> error_;
 
   network::TestURLLoaderFactory url_loader_factory_;
 };
@@ -79,6 +88,9 @@ TEST_F(AuctionDownloaderTest, NetworkError) {
   url_loader_factory_.AddResponse(url_, nullptr /* head */, kAsciiResponseBody,
                                   status);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Failed to load https://url.test/script.js error = net::ERR_FAILED.",
+      last_error_msg());
 }
 
 // HTTP 404 responses are trested as failures.
@@ -88,6 +100,9 @@ TEST_F(AuctionDownloaderTest, HttpError) {
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
               kAsciiResponseBody, kAllowFledgeHeader, net::HTTP_NOT_FOUND);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Failed to load https://url.test/script.js HTTP status = 404 Not Found.",
+      last_error_msg());
 }
 
 TEST_F(AuctionDownloaderTest, AllowFledge) {
@@ -102,26 +117,50 @@ TEST_F(AuctionDownloaderTest, AllowFledge) {
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
               kAsciiResponseBody, "X-Allow-FLEDGE: false");
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to lack of "
+      "X-Allow-FLEDGE: true.",
+      last_error_msg());
 
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
               kAsciiResponseBody, "X-Allow-FLEDGE: sometimes");
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to lack of "
+      "X-Allow-FLEDGE: true.",
+      last_error_msg());
 
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
               kAsciiResponseBody, "X-Allow-FLEDGE: ");
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to lack of "
+      "X-Allow-FLEDGE: true.",
+      last_error_msg());
 
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
               kAsciiResponseBody, "X-Allow-Hats: true");
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to lack of "
+      "X-Allow-FLEDGE: true.",
+      last_error_msg());
 
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
               kAsciiResponseBody, "");
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to lack of "
+      "X-Allow-FLEDGE: true.",
+      last_error_msg());
 
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
               kAsciiResponseBody, base::nullopt);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to lack of "
+      "X-Allow-FLEDGE: true.",
+      last_error_msg());
 }
 
 // Redirect responses are treated as failures.
@@ -140,6 +179,8 @@ TEST_F(AuctionDownloaderTest, Redirect) {
               kAsciiResponseBody, kAllowFledgeHeader, net::HTTP_OK,
               std::move(redirects));
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ("Unexpected redirect on https://url.test/script.js.",
+            last_error_msg());
 }
 
 TEST_F(AuctionDownloaderTest, Success) {
@@ -156,40 +197,72 @@ TEST_F(AuctionDownloaderTest, MimeType) {
   AddResponse(&url_loader_factory_, url_, kJsonMimeType, kUtf8Charset,
               kAsciiResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected MIME "
+      "type.",
+      last_error_msg());
 
   // Javascript request, no response type.
   AddResponse(&url_loader_factory_, url_, base::nullopt, kUtf8Charset,
               kAsciiResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected MIME "
+      "type.",
+      last_error_msg());
 
   // Javascript request, empty response type.
   AddResponse(&url_loader_factory_, url_, "", kUtf8Charset, kAsciiResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected MIME "
+      "type.",
+      last_error_msg());
 
   // Javascript request, unknown response type.
   AddResponse(&url_loader_factory_, url_, "blobfish", kUtf8Charset,
               kAsciiResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected MIME "
+      "type.",
+      last_error_msg());
 
   // JSON request, Javascript response type.
   mime_type_ = AuctionDownloader::MimeType::kJson;
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
               kAsciiResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected MIME "
+      "type.",
+      last_error_msg());
 
   // JSON request, no response type.
   AddResponse(&url_loader_factory_, url_, base::nullopt, kUtf8Charset,
               kAsciiResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected MIME "
+      "type.",
+      last_error_msg());
 
   // JSON request, empty response type.
   AddResponse(&url_loader_factory_, url_, "", kUtf8Charset, kAsciiResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected MIME "
+      "type.",
+      last_error_msg());
 
   // JSON request, unknown response type.
   AddResponse(&url_loader_factory_, url_, "blobfish", kUtf8Charset,
               kAsciiResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected MIME "
+      "type.",
+      last_error_msg());
 
   // JSON request, JSON response type.
   mime_type_ = AuctionDownloader::MimeType::kJson;
@@ -241,6 +314,10 @@ TEST_F(AuctionDownloaderTest, MimeTypeVariants) {
     AddResponse(&url_loader_factory_, url_, javascript_type, kUtf8Charset,
                 kAsciiResponseBody);
     EXPECT_FALSE(RunRequest());
+    EXPECT_EQ(
+        "Rejecting load of https://url.test/script.js due to unexpected MIME "
+        "type.",
+        last_error_msg());
   }
 
   for (const char* json_type : kJsonMimeTypes) {
@@ -248,6 +325,10 @@ TEST_F(AuctionDownloaderTest, MimeTypeVariants) {
     AddResponse(&url_loader_factory_, url_, json_type, kUtf8Charset,
                 kAsciiResponseBody);
     EXPECT_FALSE(RunRequest());
+    EXPECT_EQ(
+        "Rejecting load of https://url.test/script.js due to unexpected MIME "
+        "type.",
+        last_error_msg());
 
     mime_type_ = AuctionDownloader::MimeType::kJson;
     AddResponse(&url_loader_factory_, url_, json_type, kUtf8Charset,
@@ -259,18 +340,31 @@ TEST_F(AuctionDownloaderTest, MimeTypeVariants) {
 }
 
 TEST_F(AuctionDownloaderTest, Charset) {
+  mime_type_ = AuctionDownloader::MimeType::kJson;
   // Unknown/unsupported charsets should result in failure.
   AddResponse(&url_loader_factory_, url_, kJsonMimeType, "fred",
               kAsciiResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected charset.",
+      last_error_msg());
+
   AddResponse(&url_loader_factory_, url_, kJsonMimeType, "iso-8859-1",
               kAsciiResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected charset.",
+      last_error_msg());
 
   // ASCII charset should restrict response bodies to ASCII characters.
+  mime_type_ = AuctionDownloader::MimeType::kJavascript;
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kAsciiCharset,
               kUtf8ResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected charset.",
+      last_error_msg());
+
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kAsciiCharset,
               kAsciiResponseBody);
   std::unique_ptr<std::string> body = RunRequest();
@@ -279,9 +373,16 @@ TEST_F(AuctionDownloaderTest, Charset) {
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kAsciiCharset,
               kUtf8ResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected charset.",
+      last_error_msg());
+
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kAsciiCharset,
               kNonUtf8ResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected charset.",
+      last_error_msg());
 
   // UTF-8 charset should restrict response bodies to valid UTF-8 characters.
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
@@ -297,6 +398,9 @@ TEST_F(AuctionDownloaderTest, Charset) {
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
               kNonUtf8ResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected charset.",
+      last_error_msg());
 
   // Null charset should act like UTF-8.
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, base::nullopt,
@@ -312,6 +416,9 @@ TEST_F(AuctionDownloaderTest, Charset) {
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, base::nullopt,
               kNonUtf8ResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected charset.",
+      last_error_msg());
 
   // Empty charset should act like UTF-8.
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, "",
@@ -327,6 +434,9 @@ TEST_F(AuctionDownloaderTest, Charset) {
   AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, "",
               kNonUtf8ResponseBody);
   EXPECT_FALSE(RunRequest());
+  EXPECT_EQ(
+      "Rejecting load of https://url.test/script.js due to unexpected charset.",
+      last_error_msg());
 }
 
 }  // namespace
diff --git a/content/services/auction_worklet/auction_runner.cc b/content/services/auction_worklet/auction_runner.cc
index da5d9123c9d52..8aa3b2abdbdc7 100644
--- a/content/services/auction_worklet/auction_runner.cc
+++ b/content/services/auction_worklet/auction_runner.cc
@@ -80,7 +80,7 @@ void AuctionRunner::StartBidding() {
               base::BindOnce(&AuctionRunner::OnTrustedSignalsLoaded,
                              base::Unretained(this), bid_state));
     } else {
-      OnTrustedSignalsLoaded(bid_state, false);
+      OnTrustedSignalsLoaded(bid_state, false, base::nullopt);
     }
   }
 
@@ -92,9 +92,16 @@ void AuctionRunner::StartBidding() {
                      base::Unretained(this)));
 }
 
-void AuctionRunner::OnBidderScriptLoaded(BidState* state, bool load_result) {
+void AuctionRunner::OnBidderScriptLoaded(
+    BidState* state,
+    bool load_result,
+    base::Optional<std::string> error_msg) {
   DCHECK(!state->bidder_script_loaded);
   DCHECK(!state->failed);
+
+  if (error_msg.has_value())
+    errors_.push_back(std::move(error_msg).value());
+
   state->bidder_script_loaded = true;
   if (!load_result) {
     state->failed = true;
@@ -108,8 +115,15 @@ void AuctionRunner::OnBidderScriptLoaded(BidState* state, bool load_result) {
   MaybeRunBid(state);
 }
 
-void AuctionRunner::OnTrustedSignalsLoaded(BidState* state, bool load_result) {
+void AuctionRunner::OnTrustedSignalsLoaded(
+    BidState* state,
+    bool load_result,
+    base::Optional<std::string> error_msg) {
   DCHECK(!state->trusted_signals_loaded);
+
+  if (error_msg.has_value())
+    errors_.push_back(std::move(error_msg).value());
+
   state->trusted_signals_loaded = true;
   if (!load_result)
     state->trusted_bidding_signals.reset();
@@ -133,10 +147,17 @@ void AuctionRunner::RunBid(BidState* state) {
   base::TimeTicks start = base::TimeTicks::Now();
   state->bid_result =
       state->bidder_worklet->GenerateBid(state->trusted_bidding_signals.get());
+  if (state->bid_result.error_msg.has_value())
+    errors_.push_back(std::move(state->bid_result.error_msg).value());
   state->bid_duration = base::TimeTicks::Now() - start;
 }
 
-void AuctionRunner::OnSellerWorkletLoaded(bool load_result) {
+void AuctionRunner::OnSellerWorkletLoaded(
+    bool load_result,
+    base::Optional<std::string> error_msg) {
+  if (error_msg.has_value())
+    errors_.push_back(std::move(error_msg).value());
+
   if (load_result) {
     seller_loaded_ = true;
     if (ReadyToScore())
@@ -177,10 +198,13 @@ void AuctionRunner::ScoreOne() {
 }
 
 SellerWorklet::ScoreResult AuctionRunner::ScoreBid(const BidState* state) {
-  return seller_worklet_->ScoreAd(
+  SellerWorklet::ScoreResult result = seller_worklet_->ScoreAd(
       state->bid_result.ad, state->bid_result.bid, *auction_config_,
       browser_signals_->top_frame_origin.host(), state->bidder->group->owner,
       AdRenderFingerprint(state), state->bid_duration);
+  if (result.error_msg.has_value())
+    errors_.push_back(std::move(result.error_msg).value());
+  return result;
 }
 
 std::string AuctionRunner::AdRenderFingerprint(const BidState* state) {
@@ -225,26 +249,32 @@ void AuctionRunner::CompleteAuction() {
 
 SellerWorklet::Report AuctionRunner::ReportSellerResult(
     const BidState* best_bid) {
-  return seller_worklet_->ReportResult(
+  SellerWorklet::Report result = seller_worklet_->ReportResult(
       *auction_config_, browser_signals_->top_frame_origin.host(),
       best_bid->bidder->group->owner, best_bid->bid_result.render_url,
       AdRenderFingerprint(best_bid), best_bid->bid_result.bid,
       best_bid->score_result.score);
+  if (result.error_msg.has_value())
+    errors_.push_back(std::move(result.error_msg).value());
+  return result;
 }
 
 BidderWorklet::ReportWinResult AuctionRunner::ReportBidWin(
     const BidState* best_bid,
     const SellerWorklet::Report& seller_report) {
-  return best_bid->bidder_worklet->ReportWin(
+  BidderWorklet::ReportWinResult result = best_bid->bidder_worklet->ReportWin(
       seller_report.signals_for_winner, best_bid->bid_result.render_url,
       AdRenderFingerprint(best_bid), best_bid->bid_result.bid);
+  if (result.error_msg.has_value())
+    errors_.push_back(std::move(result.error_msg).value());
+  return result;
 }
 
 void AuctionRunner::FailAuction() {
   std::move(callback_).Run(
       GURL(), url::Origin(), std::string(),
       mojom::WinningBidderReport::New(false /* success */, GURL()),
-      mojom::SellerReport::New(false /* success */, "", GURL()));
+      mojom::SellerReport::New(false /* success */, "", GURL()), errors_);
   delete this;
 }
 
@@ -259,7 +289,8 @@ void AuctionRunner::ReportSuccess(
                                       bidder_report.report_url),
       mojom::SellerReport::New(seller_report.success,
                                seller_report.signals_for_winner,
-                               seller_report.report_url));
+                               seller_report.report_url),
+      errors_);
   delete this;
 }
 
diff --git a/content/services/auction_worklet/auction_runner.h b/content/services/auction_worklet/auction_runner.h
index a2c85ae9aa5f8..f72a3ae9abea2 100644
--- a/content/services/auction_worklet/auction_runner.h
+++ b/content/services/auction_worklet/auction_runner.h
@@ -5,6 +5,7 @@
 #ifndef CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_RUNNER_H_
 #define CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_RUNNER_H_
 
+#include <string>
 #include <vector>
 
 #include "base/callback_forward.h"
@@ -85,14 +86,19 @@ class AuctionRunner {
   ~AuctionRunner();
 
   void StartBidding();
-  void OnBidderScriptLoaded(BidState* state, bool load_result);
-  void OnTrustedSignalsLoaded(BidState* state, bool load_result);
+  void OnBidderScriptLoaded(BidState* state,
+                            bool load_result,
+                            base::Optional<std::string> error_msg);
+  void OnTrustedSignalsLoaded(BidState* state,
+                              bool load_result,
+                              base::Optional<std::string> error_msg);
   void MaybeRunBid(BidState* state);
   void RunBid(BidState* state);
 
   // True if all bid results and the seller script load are complete.
   bool ReadyToScore() const { return outstanding_bids_ == 0 && seller_loaded_; }
-  void OnSellerWorkletLoaded(bool load_result);
+  void OnSellerWorkletLoaded(bool load_result,
+                             base::Optional<std::string> error_msg);
 
   // Lets the seller score a single outstanding bid, if any, and then either
   // re-queues itself on event loop if there is more to check, or proceeds to
@@ -145,6 +151,9 @@ class AuctionRunner {
   // that can be done.
   bool seller_loaded_ = false;
   size_t seller_considering_ = 0;
+
+  // All errors reported by worklets thus far.
+  std::vector<std::string> errors_;
 };
 
 }  // namespace auction_worklet
diff --git a/content/services/auction_worklet/auction_runner_unittest.cc b/content/services/auction_worklet/auction_runner_unittest.cc
index 9b0042970dcca..f1157876f816e 100644
--- a/content/services/auction_worklet/auction_runner_unittest.cc
+++ b/content/services/auction_worklet/auction_runner_unittest.cc
@@ -14,6 +14,7 @@
 #include "content/services/auction_worklet/worklet_test_util.h"
 #include "net/http/http_status_code.h"
 #include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock-matchers.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace auction_worklet {
@@ -205,6 +206,7 @@ class AuctionRunnerTest : public testing::Test {
     std::string interest_group_name;
     mojom::WinningBidderReportPtr bidder_report;
     mojom::SellerReportPtr seller_report;
+    std::vector<std::string> errors;
   };
 
   Result RunAuctionAndWait(const GURL& seller_decision_logic_url,
@@ -238,12 +240,14 @@ class AuctionRunnerTest : public testing::Test {
                                        const url::Origin& interest_group_owner,
                                        const std::string& interest_group_name,
                                        mojom::WinningBidderReportPtr bid_report,
-                                       mojom::SellerReportPtr seller_report) {
+                                       mojom::SellerReportPtr seller_report,
+                                       const std::vector<std::string>& errors) {
           result.ad_url = ad_url;
           result.interest_group_owner = interest_group_owner;
           result.interest_group_name = interest_group_name;
           result.bidder_report = std::move(bid_report);
           result.seller_report = std::move(seller_report);
+          result.errors = errors;
           run_loop.Quit();
         }));
     run_loop.Run();
@@ -346,6 +350,7 @@ TEST_F(AuctionRunnerTest, Basic) {
   EXPECT_TRUE(res.bidder_report->report_requested);
   EXPECT_EQ("https://buyer-reporting.example.com/",
             res.bidder_report->report_url.spec());
+  EXPECT_THAT(res.errors, testing::ElementsAre());
 }
 
 // An auction where one bid is successful, another's script 404s.
@@ -381,6 +386,10 @@ TEST_F(AuctionRunnerTest, OneBidOne404) {
   EXPECT_TRUE(res.bidder_report->report_requested);
   EXPECT_EQ("https://buyer-reporting.example.com/",
             res.bidder_report->report_url.spec());
+  EXPECT_THAT(
+      res.errors,
+      testing::ElementsAre("Failed to load https://anotheradthing.com/bids.js "
+                           "HTTP status = 404 Not Found."));
 }
 
 // An auction where one bid is successful, another's script does not provide a
@@ -419,6 +428,9 @@ TEST_F(AuctionRunnerTest, OneBidOneNotMade) {
   EXPECT_TRUE(res.bidder_report->report_requested);
   EXPECT_EQ("https://buyer-reporting.example.com/",
             res.bidder_report->report_url.spec());
+  EXPECT_THAT(res.errors,
+              testing::ElementsAre("https://anotheradthing.com/bids.js "
+                                   "`generateBid` is not a function."));
 }
 
 // An auction where no bidding scripts load successfully.
@@ -444,6 +456,12 @@ TEST_F(AuctionRunnerTest, NoBids) {
   EXPECT_EQ("", res.seller_report->signals_for_winner_json);
   EXPECT_FALSE(res.bidder_report->report_requested);
   EXPECT_TRUE(res.bidder_report->report_url.is_empty());
+  EXPECT_THAT(
+      res.errors,
+      testing::ElementsAre("Failed to load https://adplatform.com/offers.js "
+                           "HTTP status = 404 Not Found.",
+                           "Failed to load https://anotheradthing.com/bids.js "
+                           "HTTP status = 404 Not Found."));
 }
 
 // An auction where none of the bidding scripts has a valid bidding function.
@@ -470,6 +488,12 @@ TEST_F(AuctionRunnerTest, NoBidMadeByScript) {
   EXPECT_EQ("", res.seller_report->signals_for_winner_json);
   EXPECT_FALSE(res.bidder_report->report_requested);
   EXPECT_TRUE(res.bidder_report->report_url.is_empty());
+  EXPECT_THAT(
+      res.errors,
+      testing::ElementsAre(
+          "https://adplatform.com/offers.js `generateBid` is not a function.",
+          "https://anotheradthing.com/bids.js `generateBid` is not a "
+          "function."));
 }
 
 // An auction where the seller script doesn't have a scoring function.
@@ -503,6 +527,11 @@ TEST_F(AuctionRunnerTest, SellerRejectsAll) {
   EXPECT_EQ("", res.seller_report->signals_for_winner_json);
   EXPECT_FALSE(res.bidder_report->report_requested);
   EXPECT_TRUE(res.bidder_report->report_url.is_empty());
+  EXPECT_THAT(res.errors,
+              testing::ElementsAre("https://adstuff.publisher1.com/auction.js "
+                                   "`scoreAd` is not a function.",
+                                   "https://adstuff.publisher1.com/auction.js "
+                                   "`scoreAd` is not a function."));
 }
 
 // An auction where seller rejects one bid when scoring.
@@ -560,6 +589,10 @@ TEST_F(AuctionRunnerTest, NoSellerScript) {
   EXPECT_TRUE(res.bidder_report->report_url.is_empty());
 
   EXPECT_EQ(0, url_loader_factory_.NumPending());
+  EXPECT_THAT(res.errors,
+              testing::ElementsAre(
+                  "Failed to load https://adstuff.publisher1.com/auction.js "
+                  "HTTP status = 404 Not Found."));
 }
 
 // An auction where bidders don't requested trusted bidding signals.
@@ -604,6 +637,7 @@ TEST_F(AuctionRunnerTest, NoTrustedBiddingSignals) {
   EXPECT_TRUE(res.bidder_report->report_requested);
   EXPECT_EQ("https://buyer-reporting.example.com/",
             res.bidder_report->report_url.spec());
+  EXPECT_THAT(res.errors, testing::ElementsAre());
 }
 
 // An auction where trusted bidding signals are requested, but the fetch 404s.
@@ -640,6 +674,15 @@ TEST_F(AuctionRunnerTest, TrustedBiddingSignals404) {
   EXPECT_TRUE(res.bidder_report->report_requested);
   EXPECT_EQ("https://buyer-reporting.example.com/",
             res.bidder_report->report_url.spec());
+  EXPECT_THAT(res.errors,
+              testing::ElementsAre("Failed to load "
+                                   "https://trustedsignaller.org/"
+                                   "signals?hostname=publisher1.com&keys=k1,k2 "
+                                   "HTTP status = 404 Not Found.",
+                                   "Failed to load "
+                                   "https://trustedsignaller.org/"
+                                   "signals?hostname=publisher1.com&keys=l1,l2 "
+                                   "HTTP status = 404 Not Found."));
 }
 
 // A successful auction where seller reporting worklet doesn't set a URL.
@@ -678,6 +721,7 @@ TEST_F(AuctionRunnerTest, NoReportResultUrl) {
   EXPECT_TRUE(res.bidder_report->report_requested);
   EXPECT_EQ("https://buyer-reporting.example.com/",
             res.bidder_report->report_url.spec());
+  EXPECT_THAT(res.errors, testing::ElementsAre());
 }
 
 // A successful auction where bidder reporting worklet doesn't set a URL.
@@ -717,6 +761,7 @@ TEST_F(AuctionRunnerTest, NoReportWinUrl) {
             res.seller_report->signals_for_winner_json);
   EXPECT_FALSE(res.bidder_report->report_requested);
   EXPECT_TRUE(res.bidder_report->report_url.is_empty());
+  EXPECT_THAT(res.errors, testing::ElementsAre());
 }
 
 // A successful auction where neither reporting worklets sets a URL.
@@ -756,6 +801,7 @@ TEST_F(AuctionRunnerTest, NeitherReportUrl) {
             res.seller_report->signals_for_winner_json);
   EXPECT_FALSE(res.bidder_report->report_requested);
   EXPECT_TRUE(res.bidder_report->report_url.is_empty());
+  EXPECT_THAT(res.errors, testing::ElementsAre());
 }
 
 }  // namespace
diff --git a/content/services/auction_worklet/auction_v8_helper.cc b/content/services/auction_worklet/auction_v8_helper.cc
index 71dcf4df4b856..5bff8e4630de5 100644
--- a/content/services/auction_worklet/auction_v8_helper.cc
+++ b/content/services/auction_worklet/auction_v8_helper.cc
@@ -10,6 +10,7 @@
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
 #include "base/sequenced_task_runner.h"
+#include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
 #include "base/synchronization/lock.h"
@@ -275,7 +276,8 @@ bool AuctionV8Helper::ExtractJson(v8::Local<v8::Context> context,
 
 v8::MaybeLocal<v8::UnboundScript> AuctionV8Helper::Compile(
     const std::string& src,
-    const GURL& src_url) {
+    const GURL& src_url,
+    base::Optional<std::string>& error_out) {
   v8::Isolate* v8_isolate = isolate();
 
   v8::MaybeLocal<v8::String> src_string = CreateUtf8String(src);
@@ -291,8 +293,10 @@ v8::MaybeLocal<v8::UnboundScript> AuctionV8Helper::Compile(
   auto result = v8::ScriptCompiler::CompileUnboundScript(
       v8_isolate, &script_source, v8::ScriptCompiler::kNoCompileOptions,
       v8::ScriptCompiler::NoCacheReason::kNoCacheNoReason);
-  if (try_catch.HasCaught())
-    PrintMessage(v8_isolate->GetCurrentContext(), try_catch.Message());
+  if (try_catch.HasCaught()) {
+    error_out = FormatExceptionMessage(v8_isolate->GetCurrentContext(),
+                                       try_catch.Message());
+  }
   return result;
 }
 
@@ -300,7 +304,8 @@ v8::MaybeLocal<v8::Value> AuctionV8Helper::RunScript(
     v8::Local<v8::Context> context,
     v8::Local<v8::UnboundScript> script,
     base::StringPiece script_name,
-    base::span<v8::Local<v8::Value>> args) {
+    base::span<v8::Local<v8::Value>> args,
+    base::Optional<std::string>& error_out) {
   DCHECK_EQ(isolate(), context->GetIsolate());
 
   v8::Local<v8::String> v8_script_name;
@@ -313,8 +318,15 @@ v8::MaybeLocal<v8::Value> AuctionV8Helper::RunScript(
   v8::TryCatch try_catch(isolate());
   ScriptTimeoutHelper timeout_helper(isolate(), script_timeout_);
   auto result = local_script->Run(context);
+
+  if (try_catch.HasTerminated()) {
+    error_out = base::StrCat({FormatValue(isolate(), script->GetScriptName()),
+                              " top-level execution timed out."});
+    return v8::MaybeLocal<v8::Value>();
+  }
+
   if (try_catch.HasCaught()) {
-    PrintMessage(context, try_catch.Message());
+    error_out = FormatExceptionMessage(context, try_catch.Message());
     return v8::MaybeLocal<v8::Value>();
   }
 
@@ -322,35 +334,47 @@ v8::MaybeLocal<v8::Value> AuctionV8Helper::RunScript(
     return v8::MaybeLocal<v8::Value>();
 
   v8::Local<v8::Value> function;
-  if (!context->Global()->Get(context, v8_script_name).ToLocal(&function))
+  if (!context->Global()->Get(context, v8_script_name).ToLocal(&function)) {
+    error_out = base::StrCat({FormatValue(isolate(), script->GetScriptName()),
+                              " function `", script_name, "` not found."});
     return v8::MaybeLocal<v8::Value>();
+  }
 
-  if (!function->IsFunction())
+  if (!function->IsFunction()) {
+    error_out = base::StrCat({FormatValue(isolate(), script->GetScriptName()),
+                              " `", script_name, "` is not a function."});
     return v8::MaybeLocal<v8::Value>();
+  }
 
   v8::MaybeLocal<v8::Value> func_result = v8::Function::Cast(*function)->Call(
       context, context->Global(), args.size(), args.data());
+  if (try_catch.HasTerminated()) {
+    error_out = base::StrCat({FormatValue(isolate(), script->GetScriptName()),
+                              " execution of `", script_name, "` timed out."});
+    return v8::MaybeLocal<v8::Value>();
+  }
   if (try_catch.HasCaught()) {
-    PrintMessage(context, try_catch.Message());
+    error_out = FormatExceptionMessage(context, try_catch.Message());
     return v8::MaybeLocal<v8::Value>();
   }
   return func_result;
 }
 
 // static
-void AuctionV8Helper::PrintMessage(v8::Local<v8::Context> context,
-                                   v8::Local<v8::Message> message) {
+std::string AuctionV8Helper::FormatExceptionMessage(
+    v8::Local<v8::Context> context,
+    v8::Local<v8::Message> message) {
   if (message.IsEmpty()) {
-    LOG(ERROR) << "Unknown exception";
+    return "Unknown exception.";
   } else {
     v8::Isolate* isolate = message->GetIsolate();
     int line_num;
-    LOG(ERROR) << FormatValue(isolate, message->GetScriptResourceName())
-               << (!context.IsEmpty() &&
-                           message->GetLineNumber(context).To(&line_num)
-                       ? std::string(":") + base::NumberToString(line_num)
-                       : std::string())
-               << " " << FormatValue(isolate, message->Get());
+    return base::StrCat(
+        {FormatValue(isolate, message->GetScriptResourceName()),
+         !context.IsEmpty() && message->GetLineNumber(context).To(&line_num)
+             ? std::string(":") + base::NumberToString(line_num)
+             : std::string(),
+         " ", FormatValue(isolate, message->Get()), "."});
   }
 }
 
diff --git a/content/services/auction_worklet/auction_v8_helper.h b/content/services/auction_worklet/auction_v8_helper.h
index 663f237f12495..58882422733f7 100644
--- a/content/services/auction_worklet/auction_v8_helper.h
+++ b/content/services/auction_worklet/auction_v8_helper.h
@@ -6,9 +6,11 @@
 #define CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_V8_HELPER_H_
 
 #include <memory>
+#include <string>
 
 #include "base/compiler_specific.h"
 #include "base/containers/span.h"
+#include "base/optional.h"
 #include "base/strings/string_piece.h"
 #include "base/time/time.h"
 #include "gin/public/isolate_holder.h"
@@ -108,9 +110,12 @@ class AuctionV8Helper {
                    std::string* out);
 
   // Compiles the provided script. Despite not being bound to a context, there
-  // still must be an active context for this method to be invoked.
-  v8::MaybeLocal<v8::UnboundScript> Compile(const std::string& src,
-                                            const GURL& src_url);
+  // still must be an active context for this method to be invoked. In case of
+  // an error, sets `error_out`.
+  v8::MaybeLocal<v8::UnboundScript> Compile(
+      const std::string& src,
+      const GURL& src_url,
+      base::Optional<std::string>& error_out);
 
   // Binds a script and runs it in the passed in context, returning the result.
   // Note that the returned value could include references to objects or
@@ -122,19 +127,21 @@ class AuctionV8Helper {
   //
   // Running this multiple times in the same context will re-load the entire
   // script file in the context, and then run the script again.
-  v8::MaybeLocal<v8::Value> RunScript(
-      v8::Local<v8::Context> context,
-      v8::Local<v8::UnboundScript> script,
-      base::StringPiece script_name,
-      base::span<v8::Local<v8::Value>> args = {});
+  //
+  // In case of an error, sets `error_out`.
+  v8::MaybeLocal<v8::Value> RunScript(v8::Local<v8::Context> context,
+                                      v8::Local<v8::UnboundScript> script,
+                                      base::StringPiece script_name,
+                                      base::span<v8::Local<v8::Value>> args,
+                                      base::Optional<std::string>& error_out);
 
   void set_script_timeout_for_testing(base::TimeDelta script_timeout) {
     script_timeout_ = script_timeout;
   }
 
  private:
-  static void PrintMessage(v8::Local<v8::Context> context,
-                           v8::Local<v8::Message> message);
+  static std::string FormatExceptionMessage(v8::Local<v8::Context> context,
+                                            v8::Local<v8::Message> message);
   static std::string FormatValue(v8::Isolate* isolate,
                                  v8::Local<v8::Value> val);
 
diff --git a/content/services/auction_worklet/auction_v8_helper_unittest.cc b/content/services/auction_worklet/auction_v8_helper_unittest.cc
index 418d34ab16294..ae37ddb095ab3 100644
--- a/content/services/auction_worklet/auction_v8_helper_unittest.cc
+++ b/content/services/auction_worklet/auction_v8_helper_unittest.cc
@@ -6,13 +6,18 @@
 
 #include <string>
 
+#include "base/optional.h"
 #include "base/test/task_environment.h"
 #include "base/time/time.h"
 #include "gin/converter.h"
+#include "testing/gmock/include/gmock/gmock-matchers.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 #include "v8/include/v8.h"
 
+using testing::HasSubstr;
+using testing::StartsWith;
+
 namespace auction_worklet {
 
 class AuctionV8HelperTest : public testing::Test {
@@ -32,53 +37,77 @@ TEST_F(AuctionV8HelperTest, Basic) {
   v8::Local<v8::UnboundScript> script;
   {
     v8::Context::Scope ctx(helper_.scratch_context());
-    ASSERT_TRUE(
-        helper_
-            .Compile("function foo() { return 1;}", GURL("https://foo.test/"))
-            .ToLocal(&script));
+    base::Optional<std::string> error_msg;
+    ASSERT_TRUE(helper_
+                    .Compile("function foo() { return 1;}",
+                             GURL("https://foo.test/"), error_msg)
+                    .ToLocal(&script));
+    EXPECT_FALSE(error_msg.has_value());
   }
 
   for (v8::Local<v8::Context> context :
        {helper_.scratch_context(), helper_.CreateContext()}) {
+    base::Optional<std::string> error_msg;
     v8::Context::Scope ctx(context);
     v8::Local<v8::Value> result;
-    ASSERT_TRUE(helper_.RunScript(context, script, "foo").ToLocal(&result));
+    ASSERT_TRUE(helper_
+                    .RunScript(context, script, "foo",
+                               base::span<v8::Local<v8::Value>>(), error_msg)
+                    .ToLocal(&result));
     int int_result = 0;
     ASSERT_TRUE(gin::ConvertFromV8(helper_.isolate(), result, &int_result));
     EXPECT_EQ(1, int_result);
+    EXPECT_FALSE(error_msg.has_value());
   }
 }
 
 // Check that timing out scripts works.
 TEST_F(AuctionV8HelperTest, Timeout) {
-  const char* kHangingScripts[] = {
+  struct HangingScript {
+    const char* script;
+    bool top_level_hangs;
+  };
+
+  const HangingScript kHangingScripts[] = {
       // Script that times out when run. Its foo() method returns 1, but should
       // never be called.
-      R"(function foo() { return 1;}
-         while(1);)",
+      {R"(function foo() { return 1;}
+        while(1);)",
+       true},
+
       // Script that times out when foo() is called.
-      "function foo() {while (1);}",
+      {"function foo() {while (1);}", false},
+
       // Script that times out when run and when foo is called.
-      R"(function foo() {while (1);}
-         while(1);)",
-  };
+      {R"(function foo() {while (1);}
+        while(1);)",
+       true}};
 
   // Use a sorter timeout so test runs faster.
   const base::TimeDelta kScriptTimeout = base::TimeDelta::FromMilliseconds(20);
   helper_.set_script_timeout_for_testing(kScriptTimeout);
 
-  for (const char* hanging_script : kHangingScripts) {
+  for (const HangingScript& hanging_script : kHangingScripts) {
     base::TimeTicks start_time = base::TimeTicks::Now();
     v8::Local<v8::Context> context = helper_.CreateContext();
     v8::Context::Scope context_scope(context);
 
     v8::Local<v8::UnboundScript> script;
-    ASSERT_TRUE(helper_.Compile(hanging_script, GURL("https://foo.test/"))
+    base::Optional<std::string> error_msg;
+    ASSERT_TRUE(helper_
+                    .Compile(hanging_script.script, GURL("https://foo.test/"),
+                             error_msg)
                     .ToLocal(&script));
+    EXPECT_FALSE(error_msg.has_value());
 
-    v8::MaybeLocal<v8::Value> result =
-        helper_.RunScript(context, script, "foo");
+    v8::MaybeLocal<v8::Value> result = helper_.RunScript(
+        context, script, "foo", base::span<v8::Local<v8::Value>>(), error_msg);
     EXPECT_TRUE(result.IsEmpty());
+    ASSERT_TRUE(error_msg.has_value());
+    EXPECT_EQ(hanging_script.top_level_hangs
+                  ? "https://foo.test/ top-level execution timed out."
+                  : "https://foo.test/ execution of `foo` timed out.",
+              error_msg.value());
 
     // Make sure at least `kScriptTimeout` has passed, allowing for some time
     // skew between change in base::TimeTicks::Now() and the timeout. This
@@ -93,11 +122,18 @@ TEST_F(AuctionV8HelperTest, Timeout) {
   v8::Local<v8::Context> context = helper_.CreateContext();
   v8::Context::Scope context_scope(context);
   v8::Local<v8::UnboundScript> script;
-  ASSERT_TRUE(
-      helper_.Compile("function foo() { return 1;}", GURL("https://foo.test/"))
-          .ToLocal(&script));
+  base::Optional<std::string> error_msg;
+  ASSERT_TRUE(helper_
+                  .Compile("function foo() { return 1;}",
+                           GURL("https://foo.test/"), error_msg)
+                  .ToLocal(&script));
+  EXPECT_FALSE(error_msg.has_value());
   v8::Local<v8::Value> result;
-  ASSERT_TRUE(helper_.RunScript(context, script, "foo").ToLocal(&result));
+  ASSERT_TRUE(helper_
+                  .RunScript(context, script, "foo",
+                             base::span<v8::Local<v8::Value>>(), error_msg)
+                  .ToLocal(&result));
+  EXPECT_FALSE(error_msg.has_value());
   int int_result = 0;
   ASSERT_TRUE(gin::ConvertFromV8(helper_.isolate(), result, &int_result));
   EXPECT_EQ(1, int_result);
@@ -111,11 +147,116 @@ TEST_F(AuctionV8HelperTest, NoTime) {
 
   // Make sure Date() is not accessible.
   v8::Local<v8::UnboundScript> script;
+  base::Optional<std::string> error_msg;
   ASSERT_TRUE(helper_
                   .Compile("function foo() { return Date();}",
-                           GURL("https://foo.test/"))
+                           GURL("https://foo.test/"), error_msg)
                   .ToLocal(&script));
-  EXPECT_TRUE(helper_.RunScript(context, script, "foo").IsEmpty());
+  EXPECT_FALSE(error_msg.has_value());
+  EXPECT_TRUE(helper_
+                  .RunScript(context, script, "foo",
+                             base::span<v8::Local<v8::Value>>(), error_msg)
+                  .IsEmpty());
+  ASSERT_TRUE(error_msg.has_value());
+  EXPECT_THAT(error_msg.value(), StartsWith("https://foo.test/:1"));
+  EXPECT_THAT(error_msg.value(), HasSubstr("ReferenceError"));
+  EXPECT_THAT(error_msg.value(), HasSubstr("Date"));
+}
+
+// A script that doesn't compile.
+TEST_F(AuctionV8HelperTest, CompileError) {
+  v8::Local<v8::UnboundScript> script;
+  v8::Context::Scope ctx(helper_.scratch_context());
+  base::Optional<std::string> error_msg;
+  ASSERT_FALSE(
+      helper_.Compile("function foo() { ", GURL("https://foo.test/"), error_msg)
+          .ToLocal(&script));
+  ASSERT_TRUE(error_msg.has_value());
+  EXPECT_THAT(error_msg.value(), StartsWith("https://foo.test/:1 "));
+  EXPECT_THAT(error_msg.value(), HasSubstr("SyntaxError"));
+}
+
+// Test for exception at runtime at top-level.
+TEST_F(AuctionV8HelperTest, RunErrorTopLevel) {
+  v8::Local<v8::UnboundScript> script;
+  {
+    v8::Context::Scope ctx(helper_.scratch_context());
+    base::Optional<std::string> error_msg;
+    ASSERT_TRUE(helper_
+                    .Compile("\n\nthrow new Error('I am an error');",
+                             GURL("https://foo.test/"), error_msg)
+                    .ToLocal(&script));
+    EXPECT_FALSE(error_msg.has_value());
+  }
+
+  v8::Local<v8::Context> context = helper_.CreateContext();
+  base::Optional<std::string> error_msg;
+  v8::Context::Scope ctx(context);
+  v8::Local<v8::Value> result;
+  ASSERT_FALSE(helper_
+                   .RunScript(context, script, "foo",
+                              base::span<v8::Local<v8::Value>>(), error_msg)
+                   .ToLocal(&result));
+  ASSERT_TRUE(error_msg.has_value());
+  EXPECT_EQ("https://foo.test/:3 Uncaught Error: I am an error.",
+            error_msg.value());
+}
+
+// Test for when desired function isn't found
+TEST_F(AuctionV8HelperTest, TargetFunctionNotFound) {
+  v8::Local<v8::UnboundScript> script;
+  {
+    v8::Context::Scope ctx(helper_.scratch_context());
+    base::Optional<std::string> error_msg;
+    ASSERT_TRUE(helper_
+                    .Compile("function foo() { return 1;}",
+                             GURL("https://foo.test/"), error_msg)
+                    .ToLocal(&script));
+    EXPECT_FALSE(error_msg.has_value());
+  }
+
+  v8::Local<v8::Context> context = helper_.CreateContext();
+
+  base::Optional<std::string> error_msg;
+  v8::Context::Scope ctx(context);
+  v8::Local<v8::Value> result;
+  ASSERT_FALSE(helper_
+                   .RunScript(context, script, "bar",
+                              base::span<v8::Local<v8::Value>>(), error_msg)
+                   .ToLocal(&result));
+  ASSERT_TRUE(error_msg.has_value());
+
+  // This "not a function" and not "not found" since the lookup successfully
+  // returns `undefined`.
+  EXPECT_EQ("https://foo.test/ `bar` is not a function.", error_msg.value());
+}
+
+TEST_F(AuctionV8HelperTest, TargetFunctionError) {
+  v8::Local<v8::UnboundScript> script;
+  {
+    v8::Context::Scope ctx(helper_.scratch_context());
+    base::Optional<std::string> error_msg;
+    ASSERT_TRUE(helper_
+                    .Compile("function foo() { return notfound;}",
+                             GURL("https://foo.test/"), error_msg)
+                    .ToLocal(&script));
+    EXPECT_FALSE(error_msg.has_value());
+  }
+
+  v8::Local<v8::Context> context = helper_.CreateContext();
+
+  base::Optional<std::string> error_msg;
+  v8::Context::Scope ctx(context);
+  v8::Local<v8::Value> result;
+  ASSERT_FALSE(helper_
+                   .RunScript(context, script, "foo",
+                              base::span<v8::Local<v8::Value>>(), error_msg)
+                   .ToLocal(&result));
+  ASSERT_TRUE(error_msg.has_value());
+
+  EXPECT_THAT(error_msg.value(), StartsWith("https://foo.test/:1 "));
+  EXPECT_THAT(error_msg.value(), HasSubstr("ReferenceError"));
+  EXPECT_THAT(error_msg.value(), HasSubstr("notfound"));
 }
 
 }  // namespace auction_worklet
diff --git a/content/services/auction_worklet/bidder_worklet.cc b/content/services/auction_worklet/bidder_worklet.cc
index c2f73b21db129..2846f2b5f17dc 100644
--- a/content/services/auction_worklet/bidder_worklet.cc
+++ b/content/services/auction_worklet/bidder_worklet.cc
@@ -14,6 +14,7 @@
 #include "base/callback.h"
 #include "base/logging.h"
 #include "base/stl_util.h"
+#include "base/strings/strcat.h"
 #include "base/time/time.h"
 #include "content/services/auction_worklet/auction_v8_helper.h"
 #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
@@ -59,6 +60,17 @@ BidderWorklet::BidResult::BidResult(std::string ad, double bid, GURL render_url)
   DCHECK(this->render_url.is_valid());
 }
 
+BidderWorklet::BidResult::BidResult(base::Optional<std::string> error_msg)
+    : error_msg(std::move(error_msg)) {}
+
+BidderWorklet::BidResult::BidResult(const BidResult& other) = default;
+BidderWorklet::BidResult::BidResult(BidResult&& other) = default;
+BidderWorklet::BidResult::~BidResult() = default;
+BidderWorklet::BidResult& BidderWorklet::BidResult::operator=(
+    const BidResult&) = default;
+BidderWorklet::BidResult& BidderWorklet::BidResult::operator=(BidResult&&) =
+    default;
+
 BidderWorklet::ReportWinResult::ReportWinResult() = default;
 
 BidderWorklet::ReportWinResult::ReportWinResult(GURL report_url)
@@ -66,6 +78,20 @@ BidderWorklet::ReportWinResult::ReportWinResult(GURL report_url)
   DCHECK(this->report_url.is_valid());
 }
 
+BidderWorklet::ReportWinResult::ReportWinResult(
+    base::Optional<std::string> error_msg)
+    : error_msg(std::move(error_msg)) {}
+
+BidderWorklet::ReportWinResult::ReportWinResult(const ReportWinResult& other) =
+    default;
+BidderWorklet::ReportWinResult::ReportWinResult(ReportWinResult&& other) =
+    default;
+BidderWorklet::ReportWinResult::~ReportWinResult() = default;
+BidderWorklet::ReportWinResult& BidderWorklet::ReportWinResult::operator=(
+    const ReportWinResult&) = default;
+BidderWorklet::ReportWinResult& BidderWorklet::ReportWinResult::operator=(
+    ReportWinResult&&) = default;
+
 BidderWorklet::BidderWorklet(
     network::mojom::URLLoaderFactory* url_loader_factory,
     mojom::BiddingInterestGroupPtr bidding_interest_group,
@@ -76,7 +102,9 @@ BidderWorklet::BidderWorklet(
     base::Time auction_start_time,
     AuctionV8Helper* v8_helper,
     LoadWorkletCallback load_worklet_callback)
-    : v8_helper_(v8_helper),
+    : script_source_url_(
+          bidding_interest_group->group->bidding_url.value_or(GURL())),
+      v8_helper_(v8_helper),
       bidding_interest_group_(std::move(bidding_interest_group)),
       auction_signals_json_(auction_signals_json),
       per_buyer_signals_json_(per_buyer_signals_json),
@@ -85,11 +113,10 @@ BidderWorklet::BidderWorklet(
       browser_signal_seller_(browser_signal_seller),
       auction_start_time_(auction_start_time) {
   DCHECK(load_worklet_callback);
-  // TODO(mmenke): Remove up the value_or() - auction worklets shouldn't be
-  // created when there's no bidding URL.
+  // TODO(mmenke): Remove up the value_or() for script_source_url_- auction
+  // worklets shouldn't be created when there's no bidding URL.
   worklet_loader_ = std::make_unique<WorkletLoader>(
-      url_loader_factory,
-      bidding_interest_group_->group->bidding_url.value_or(GURL()), v8_helper,
+      url_loader_factory, script_source_url_, v8_helper,
       base::BindOnce(&BidderWorklet::OnDownloadComplete, base::Unretained(this),
                      std::move(load_worklet_callback)));
 }
@@ -202,12 +229,18 @@ BidderWorklet::BidResult BidderWorklet::GenerateBid(
   args.push_back(browser_signals);
 
   v8::Local<v8::Value> generate_bid_result;
+  base::Optional<std::string> error_msg_out;
   if (!v8_helper_
            ->RunScript(context, worklet_script_->Get(isolate), "generateBid",
-                       args)
-           .ToLocal(&generate_bid_result) ||
-      !generate_bid_result->IsObject()) {
-    return BidResult();
+                       args, error_msg_out)
+           .ToLocal(&generate_bid_result)) {
+    return BidResult(std::move(error_msg_out));
+  }
+
+  if (!generate_bid_result->IsObject()) {
+    return BidResult(
+        base::StrCat({script_source_url_.spec(),
+                      " generateBid() return value not an object."}));
   }
 
   gin::Dictionary result_dict(isolate, generate_bid_result.As<v8::Object>());
@@ -221,22 +254,29 @@ BidderWorklet::BidResult BidderWorklet::GenerateBid(
       !v8_helper_->ExtractJson(context, ad_object, &ad_json) ||
       !result_dict.Get("bid", &bid) ||
       !result_dict.Get("render", &render_url_string)) {
-    return BidResult();
+    return BidResult(
+        base::StrCat({script_source_url_.spec(),
+                      " generateBid() return value has incorrect structure."}));
   }
 
   if (bid <= 0 || std::isnan(bid) || !std::isfinite(bid))
     return BidResult();
 
   GURL render_url(render_url_string);
-  if (!render_url.is_valid() || !render_url.SchemeIs(url::kHttpsScheme))
-    return BidResult();
+  if (!render_url.is_valid() || !render_url.SchemeIs(url::kHttpsScheme)) {
+    return BidResult(base::StrCat(
+        {script_source_url_.spec(),
+         " generateBid() returned render_url isn't a valid https:// URL."}));
+  }
 
   // `render_url` must be in `ad_render_urls`.
   for (const auto& ad : *interest_group.ads) {
     if (render_url == ad->render_url)
       return BidResult(std::move(ad_json), bid, std::move(render_url));
   }
-  return BidResult();
+  return BidResult(base::StrCat({script_source_url_.spec(),
+                                 " generateBid() returned render_url isn't one "
+                                 "of the registered creative URLs."}));
 }
 
 BidderWorklet::ReportWinResult BidderWorklet::ReportWin(
@@ -285,10 +325,12 @@ BidderWorklet::ReportWinResult BidderWorklet::ReportWin(
 
   // An empty return value indicates an exception was thrown. Any other return
   // value indicates no exception.
+  base::Optional<std::string> error_msg_out;
   if (v8_helper_
-          ->RunScript(context, worklet_script_->Get(isolate), "reportWin", args)
+          ->RunScript(context, worklet_script_->Get(isolate), "reportWin", args,
+                      error_msg_out)
           .IsEmpty()) {
-    return ReportWinResult();
+    return ReportWinResult(std::move(error_msg_out));
   }
 
   if (!report_bindings.report_url().is_valid())
@@ -299,10 +341,12 @@ BidderWorklet::ReportWinResult BidderWorklet::ReportWin(
 
 void BidderWorklet::OnDownloadComplete(
     LoadWorkletCallback load_worklet_callback,
-    std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script) {
+    std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
+    base::Optional<std::string> error_msg) {
   worklet_loader_.reset();
   worklet_script_ = std::move(worklet_script);
-  std::move(load_worklet_callback).Run(worklet_script_ != nullptr);
+  std::move(load_worklet_callback)
+      .Run(worklet_script_ != nullptr, std::move(error_msg));
 }
 
 }  // namespace auction_worklet
diff --git a/content/services/auction_worklet/bidder_worklet.h b/content/services/auction_worklet/bidder_worklet.h
index c1d3a9fce6847..85fec55ce7c28 100644
--- a/content/services/auction_worklet/bidder_worklet.h
+++ b/content/services/auction_worklet/bidder_worklet.h
@@ -49,12 +49,21 @@ class BidderWorklet {
     // valid.
     BidResult(std::string ad, double bid, GURL render_url);
 
+    // Constructor when there is no bid due to an error and an error message
+    // may be available.
+    explicit BidResult(base::Optional<std::string> error_msg);
+
+    BidResult(const BidResult& other);
+    BidResult(BidResult&& other);
+
+    ~BidResult();
+
+    BidResult& operator=(const BidResult&);
+    BidResult& operator=(BidResult&&);
+
     // `success` will be false on any type of failure, and other values will be
     // empty or 0. Inability to extract ad string, bid value, or valid render
     // URL are all considered errors.
-    //
-    // TODO(mmenke): Pass along some sort of error string instead, to make
-    // debugging easier.
     bool success = false;
 
     // JSON string to be passed to the scoring function.
@@ -66,6 +75,10 @@ class BidderWorklet {
 
     // Render URL, if any bid was made.
     GURL render_url;
+
+    // Error message for debugging. This isn't guaranteed to be produced for all
+    // failures, so don't check this instead of `success`.
+    base::Optional<std::string> error_msg;
   };
 
   struct ReportWinResult {
@@ -76,19 +89,33 @@ class BidderWorklet {
     // Constructor when a report was requested.
     explicit ReportWinResult(GURL report_url);
 
+    // Constructor when there is some sort of a falire for which an error
+    // message may be available.
+    explicit ReportWinResult(base::Optional<std::string> error_msg);
+
+    ReportWinResult(const ReportWinResult& other);
+    ReportWinResult(ReportWinResult&& other);
+
+    ~ReportWinResult();
+
+    ReportWinResult& operator=(const ReportWinResult&);
+    ReportWinResult& operator=(ReportWinResult&&);
+
     // `success` will be false on any type of failure. Neither lack or reporting
     // function nor lack of report URL is considered an error.
-    //
-    // TODO(mmenke): Pass along some sort of error string instead, to make
-    // debugging easier.
     bool success = false;
 
     // Report URL, if one is provided. Empty on failure, or if no report URL is
     // provided.
     GURL report_url;
+
+    // Error message for debugging.
+    base::Optional<std::string> error_msg;
   };
 
-  using LoadWorkletCallback = base::OnceCallback<void(bool success)>;
+  using LoadWorkletCallback =
+      base::OnceCallback<void(bool success,
+                              base::Optional<std::string> error_msg)>;
 
   // Starts loading the worklet script on construction. Callback will be invoked
   // asynchronously once the data has been fetched or an error has occurred.
@@ -123,8 +150,10 @@ class BidderWorklet {
  private:
   void OnDownloadComplete(
       LoadWorkletCallback load_worklet_callback,
-      std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script);
+      std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
+      base::Optional<std::string> error_msg);
 
+  const GURL script_source_url_;
   AuctionV8Helper* const v8_helper_;
   const mojom::BiddingInterestGroupPtr bidding_interest_group_;
 
diff --git a/content/services/auction_worklet/bidder_worklet_unittest.cc b/content/services/auction_worklet/bidder_worklet_unittest.cc
index c18d6237d148d..99d9fd8f1327e 100644
--- a/content/services/auction_worklet/bidder_worklet_unittest.cc
+++ b/content/services/auction_worklet/bidder_worklet_unittest.cc
@@ -20,11 +20,15 @@
 #include "mojo/public/cpp/bindings/struct_ptr.h"
 #include "net/http/http_status_code.h"
 #include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock-matchers.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
 #include "url/gurl.h"
 #include "url/origin.h"
 
+using testing::HasSubstr;
+using testing::StartsWith;
+
 namespace auction_worklet {
 namespace {
 
@@ -135,31 +139,41 @@ class BidderWorkletTest : public testing::Test {
     EXPECT_EQ(expected_result.ad, actual_result.ad);
     EXPECT_EQ(expected_result.bid, actual_result.bid);
     EXPECT_EQ(expected_result.render_url, actual_result.render_url);
+    EXPECT_EQ(expected_result.error_msg.has_value(),
+              actual_result.error_msg.has_value());
+    EXPECT_EQ(expected_result.error_msg.value_or("Not an error"),
+              actual_result.error_msg.value_or("Not an error"));
   }
 
   // Configures `url_loader_factory_` to return a reportWin() script with the
   // specified body. Then runs the script, expecting the provided result.
   void RunReportWinWithFunctionBodyExpectingResult(
       const std::string& function_body,
-      const GURL& expected_report_url) {
+      const GURL& expected_report_url,
+      base::Optional<std::string> expected_error_msg = base::nullopt) {
     RunReportWinWithJavascriptExpectingResult(
-        CreateReportWinScript(function_body), expected_report_url);
+        CreateReportWinScript(function_body), expected_report_url,
+        std::move(expected_error_msg));
   }
 
   // Configures `url_loader_factory_` to return a reportWin() script with the
   // specified Javascript. Then runs the script, expecting the provided result.
   void RunReportWinWithJavascriptExpectingResult(
       const std::string& javascript,
-      const GURL& expected_report_url) {
+      const GURL& expected_report_url,
+      base::Optional<std::string> expected_error_msg = base::nullopt) {
     SCOPED_TRACE(javascript);
     AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
                           javascript);
-    RunReportWinExpectingResult(expected_report_url);
+    RunReportWinExpectingResult(expected_report_url,
+                                std::move(expected_error_msg));
   }
 
   // Loads and runs a reportWin() with the provided return line, expecting the
   // supplied result.
-  void RunReportWinExpectingResult(const GURL& expected_report_url) {
+  void RunReportWinExpectingResult(
+      const GURL& expected_report_url,
+      base::Optional<std::string> expected_error_msg = base::nullopt) {
     auto bidder_worket = CreateWorklet();
     ASSERT_TRUE(bidder_worket);
 
@@ -168,6 +182,10 @@ class BidderWorkletTest : public testing::Test {
         browser_signal_ad_render_fingerprint_, browser_signal_bid_);
     EXPECT_EQ(!expected_report_url.is_empty(), actual_result.success);
     EXPECT_EQ(expected_report_url, actual_result.report_url);
+    EXPECT_EQ(expected_error_msg.has_value(),
+              actual_result.error_msg.has_value());
+    EXPECT_EQ(expected_error_msg.value_or("Not an error"),
+              actual_result.error_msg.value_or("Not an error"));
   }
 
   // Create a BidderWorklet, waiting for the URLLoader to complete. Returns
@@ -231,11 +249,19 @@ class BidderWorkletTest : public testing::Test {
     return out;
   }
 
-  void CreateWorkletCallback(bool success) {
+  void CreateWorkletCallback(bool success,
+                             base::Optional<std::string> error_msg) {
     create_worklet_succeeded_ = success;
+    error_msg_ = std::move(error_msg);
+    if (success)
+      EXPECT_FALSE(error_msg_.has_value());
     load_script_run_loop_->Quit();
   }
 
+  std::string last_error_msg() const {
+    return error_msg_.value_or("Not an error");
+  }
+
   base::test::TaskEnvironment task_environment_;
 
   // Values used to construct the BiddingInterestGroup passed to the
@@ -277,6 +303,7 @@ class BidderWorkletTest : public testing::Test {
   // synchronously.
   std::unique_ptr<base::RunLoop> load_script_run_loop_;
   bool create_worklet_succeeded_ = false;
+  base::Optional<std::string> error_msg_;
 
   network::TestURLLoaderFactory url_loader_factory_;
   AuctionV8Helper v8_helper_;
@@ -287,12 +314,17 @@ TEST_F(BidderWorkletTest, NetworkError) {
                                   CreateBasicGenerateBidScript(),
                                   net::HTTP_NOT_FOUND);
   EXPECT_FALSE(CreateWorklet());
+  EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.",
+            last_error_msg());
 }
 
 TEST_F(BidderWorkletTest, CompileError) {
   AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
                         "Invalid Javascript");
   EXPECT_FALSE(CreateWorklet());
+
+  EXPECT_THAT(last_error_msg(), StartsWith("https://url.test/:1 "));
+  EXPECT_THAT(last_error_msg(), HasSubstr("SyntaxError"));
 }
 
 // Test parsing of return values.
@@ -335,10 +367,12 @@ TEST_F(BidderWorkletTest, GenerateBidResult) {
   // Other values JSON can't represent result in failing instead of null.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: globalThis.not_defined, bid:1, render:"https://response.test/"})",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult("https://url.test/ generateBid() return value "
+                               "has incorrect structure."));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: function() {return 1;}, bid:1, render:"https://response.test/"})",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult("https://url.test/ generateBid() return value "
+                               "has incorrect structure."));
 
   // Make sure recursive structures aren't allowed in ad field.
   RunGenerateBidWithJavascriptExpectingResult(
@@ -349,7 +383,8 @@ TEST_F(BidderWorkletTest, GenerateBidResult) {
           return {ad: a, bid:1, render:"https://response.test/"};
         }
       )",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult("https://url.test/ generateBid() return value "
+                               "has incorrect structure."));
 
   // --------
   // Vary bid
@@ -392,10 +427,12 @@ TEST_F(BidderWorkletTest, GenerateBidResult) {
   // Non-numeric bid.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:"1", render:"https://response.test/"})",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult("https://url.test/ generateBid() return value "
+                               "has incorrect structure."));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:[1], render:"https://response.test/"})",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult("https://url.test/ generateBid() return value "
+                               "has incorrect structure."));
 
   // ---------
   // Vary URL.
@@ -408,43 +445,61 @@ TEST_F(BidderWorkletTest, GenerateBidResult) {
   // Disallowed schemes.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1, render:"http://response.test/"})",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult("https://url.test/ generateBid() returned "
+                               "render_url isn't a valid https:// URL."));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1, render:"chrome-extension://response.test/"})",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult("https://url.test/ generateBid() returned "
+                               "render_url isn't a valid https:// URL."));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1, render:"about:blank"})",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult("https://url.test/ generateBid() returned "
+                               "render_url isn't a valid https:// URL."));
   RunGenerateBidWithReturnValueExpectingResult(
-      R"({ad: ["ad"], bid:1, render:"data:,foo"})", BidderWorklet::BidResult());
+      R"({ad: ["ad"], bid:1, render:"data:,foo"})",
+      BidderWorklet::BidResult("https://url.test/ generateBid() returned "
+                               "render_url isn't a valid https:// URL."));
 
   // Invalid URLs.
   RunGenerateBidWithReturnValueExpectingResult(
-      R"({ad: ["ad"], bid:1, render:"test"})", BidderWorklet::BidResult());
+      R"({ad: ["ad"], bid:1, render:"test"})",
+      BidderWorklet::BidResult("https://url.test/ generateBid() returned "
+                               "render_url isn't a valid https:// URL."));
   RunGenerateBidWithReturnValueExpectingResult(
-      R"({ad: ["ad"], bid:1, render:"http://"})", BidderWorklet::BidResult());
+      R"({ad: ["ad"], bid:1, render:"http://"})",
+      BidderWorklet::BidResult("https://url.test/ generateBid() returned "
+                               "render_url isn't a valid https:// URL."));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], bid:1, render:["http://response.test/"]})",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult("https://url.test/ generateBid() return value "
+                               "has incorrect structure."));
   RunGenerateBidWithReturnValueExpectingResult(
-      R"({ad: ["ad"], bid:1, render:9})", BidderWorklet::BidResult());
+      R"({ad: ["ad"], bid:1, render:9})",
+      BidderWorklet::BidResult("https://url.test/ generateBid() return value "
+                               "has incorrect structure."));
 
   // ------------
   // Other cases.
   // ------------
 
   // No return value.
-  RunGenerateBidWithReturnValueExpectingResult("", BidderWorklet::BidResult());
+  RunGenerateBidWithReturnValueExpectingResult(
+      "", BidderWorklet::BidResult(
+              "https://url.test/ generateBid() return value not an object."));
 
   // Missing value.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({bid:"a", render:"https://response.test/"})",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult("https://url.test/ generateBid() return value "
+                               "has incorrect structure."));
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: ["ad"], render:"https://response.test/"})",
-      BidderWorklet::BidResult());
-  RunGenerateBidWithReturnValueExpectingResult(R"({ad: ["ad"], bid:"a"})",
-                                               BidderWorklet::BidResult());
+      BidderWorklet::BidResult("https://url.test/ generateBid() return value "
+                               "has incorrect structure."));
+  RunGenerateBidWithReturnValueExpectingResult(
+      R"({ad: ["ad"], bid:"a"})",
+      BidderWorklet::BidResult("https://url.test/ generateBid() return value "
+                               "has incorrect structure."));
 
   // Valid JS, but missing function.
   RunGenerateBidWithJavascriptExpectingResult(
@@ -453,22 +508,28 @@ TEST_F(BidderWorkletTest, GenerateBidResult) {
           return {ad: ["ad"], bid:1, render:"https://response.test/"};
         }
       )",
-      BidderWorklet::BidResult());
-  RunGenerateBidWithJavascriptExpectingResult("", BidderWorklet::BidResult());
-  RunGenerateBidWithJavascriptExpectingResult("5", BidderWorklet::BidResult());
-  RunGenerateBidWithJavascriptExpectingResult("shrimp",
-                                              BidderWorklet::BidResult());
+      BidderWorklet::BidResult(
+          "https://url.test/ `generateBid` is not a function."));
+  RunGenerateBidWithJavascriptExpectingResult(
+      "", BidderWorklet::BidResult(
+              "https://url.test/ `generateBid` is not a function."));
+  RunGenerateBidWithJavascriptExpectingResult(
+      "5", BidderWorklet::BidResult(
+               "https://url.test/ `generateBid` is not a function."));
 
   // Throw exception.
-  RunGenerateBidWithReturnValueExpectingResult("shrimp",
-                                               BidderWorklet::BidResult());
+  RunGenerateBidWithJavascriptExpectingResult(
+      "shrimp",
+      BidderWorklet::BidResult("https://url.test/:1 Uncaught ReferenceError: "
+                               "shrimp is not defined."));
 }
 
 // Make sure Date() is not available when running generateBid().
 TEST_F(BidderWorkletTest, GenerateBidDateNotAvailable) {
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: Date().toString(), bid:1, render:"https://response.test/"})",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult(
+          "https://url.test/:4 Uncaught ReferenceError: Date is not defined."));
 }
 
 // Checks that most input parameters are correctly passed in, and each is parsed
@@ -610,7 +671,9 @@ TEST_F(BidderWorkletTest, GenerateBidBasicInputParameters) {
   // A bid URL that's not in the InterestGroup's ads list should fail.
   RunGenerateBidWithReturnValueExpectingResult(
       R"({ad: 0, bid:1, render:"https://response2.test/"})",
-      BidderWorklet::BidResult());
+      BidderWorklet::BidResult(
+          "https://url.test/ generateBid() returned render_url isn't one of "
+          "the registered creative URLs."));
 
   // Adding an ad with a corresponding `renderUrl` should result in success.
   // Also check the `interestGroup.ads` field passed to Javascript.
@@ -784,10 +847,12 @@ TEST_F(BidderWorkletTest, GenerateBidTrustedBiddingSignals) {
   trusted_bidding_signals_ = std::make_unique<TrustedBiddingSignals>(
       &url_loader_factory_, std::vector<std::string>({"key1", "key2"}),
       "hostname", kBaseSignalsUrl, &v8_helper_,
-      base::BindLambdaForTesting([&](bool success) {
-        signals_loaded_successfully = success;
-        run_loop.Quit();
-      }));
+      base::BindLambdaForTesting(
+          [&](bool success, base::Optional<std::string> signals_error_msg) {
+            signals_loaded_successfully = success;
+            EXPECT_FALSE(signals_error_msg.has_value());
+            run_loop.Quit();
+          }));
   run_loop.Run();
   ASSERT_TRUE(signals_loaded_successfully);
 
@@ -827,21 +892,31 @@ TEST_F(BidderWorkletTest, ReportWin) {
       R"(sendReportTo("https://foo.test/bar"))", GURL("https://foo.test/bar"));
 
   RunReportWinWithFunctionBodyExpectingResult(
-      R"(sendReportTo("http://http.not.allowed.test"))", GURL());
+      R"(sendReportTo("http://http.not.allowed.test"))", GURL(),
+      "https://url.test/:4 Uncaught TypeError: sendReportTo must be passed a "
+      "valid HTTPS url.");
   RunReportWinWithFunctionBodyExpectingResult(
-      R"(sendReportTo("file:///file.not.allowed.test"))", GURL());
+      R"(sendReportTo("file:///file.not.allowed.test"))", GURL(),
+      "https://url.test/:4 Uncaught TypeError: sendReportTo must be passed a "
+      "valid HTTPS url.");
 
-  RunReportWinWithFunctionBodyExpectingResult(R"(sendReportTo(""))", GURL());
+  RunReportWinWithFunctionBodyExpectingResult(
+      R"(sendReportTo(""))", GURL(),
+      "https://url.test/:4 Uncaught TypeError: sendReportTo must be passed a "
+      "valid HTTPS url.");
 
   RunReportWinWithFunctionBodyExpectingResult(
       R"(sendReportTo("https://foo.test");sendReportTo("https://foo.test"))",
-      GURL());
+      GURL(),
+      "https://url.test/:4 Uncaught TypeError: sendReportTo may be called at "
+      "most once.");
 }
 
 // Make sure Date() is not available when running reportWin().
 TEST_F(BidderWorkletTest, ReportWinDateNotAvailable) {
   RunReportWinWithFunctionBodyExpectingResult(
-      R"(sendReportTo("https://foo.test/" + Date().toString()))", GURL());
+      R"(sendReportTo("https://foo.test/" + Date().toString()))", GURL(),
+      "https://url.test/:4 Uncaught ReferenceError: Date is not defined.");
 }
 
 TEST_F(BidderWorkletTest, ReportWinParameters) {
@@ -852,31 +927,49 @@ TEST_F(BidderWorkletTest, ReportWinParameters) {
     bool is_json;
     // Pointer to location at which the string can be modified.
     std::string* value_ptr;
+    // Whether to expect an error. This can be empty when call fails in case
+    // it's due to something like passing non-JSON to JSON parameter which user
+    // code should be unable to trigger, and for which we thus do not produce
+    // an error message.
+    base::Optional<std::string> expect_error_msg;
+    base::Optional<std::string> expect_error_msg_array;
   } kStringTestCases[] = {
       {
           "auctionSignals",
           true /* is_json */,
           &auction_signals_,
+          base::nullopt,
+          base::nullopt,
       },
       {
           "perBuyerSignals",
           true /* is_json */,
           &per_buyer_signals_,
+          base::nullopt,
+          base::nullopt,
       },
       {
           "sellerSignals",
           true /* is_json */,
           &seller_signals_,
+          base::nullopt,
+          base::nullopt,
       },
       {
           "browserSignals.interestGroupName",
           false /* is_json */,
           &interest_group_name_,
+          base::nullopt,
+          "https://url.test/:4 Uncaught TypeError: sendReportTo must be passed "
+          "a valid HTTPS url.",
       },
       {
           "browserSignals.adRenderFingerprint",
           false /* is_json */,
           &browser_signal_ad_render_fingerprint_,
+          base::nullopt,
+          "https://url.test/:4 Uncaught TypeError: sendReportTo must be passed "
+          "a valid HTTPS url.",
       },
   };
 
@@ -886,12 +979,14 @@ TEST_F(BidderWorkletTest, ReportWinParameters) {
     *test_case.value_ptr = "https://foo.test/";
     RunReportWinWithFunctionBodyExpectingResult(
         base::StringPrintf("sendReportTo(%s)", test_case.name),
-        test_case.is_json ? GURL() : GURL("https://foo.test/"));
+        test_case.is_json ? GURL() : GURL("https://foo.test/"),
+        test_case.expect_error_msg);
 
     *test_case.value_ptr = R"(["https://foo.test/"])";
     RunReportWinWithFunctionBodyExpectingResult(
         base::StringPrintf("sendReportTo(%s[0])", test_case.name),
-        test_case.is_json ? GURL("https://foo.test/") : GURL());
+        test_case.is_json ? GURL("https://foo.test/") : GURL(),
+        test_case.expect_error_msg_array);
 
     SetDefaultParameters();
   }
diff --git a/content/services/auction_worklet/public/mojom/auction_worklet_service.mojom b/content/services/auction_worklet/public/mojom/auction_worklet_service.mojom
index 6552c606b9c7d..2b0adb60137c0 100644
--- a/content/services/auction_worklet/public/mojom/auction_worklet_service.mojom
+++ b/content/services/auction_worklet/public/mojom/auction_worklet_service.mojom
@@ -106,6 +106,9 @@ interface AuctionWorkletService {
   // `seller_report` indicates if the seller wishes to make a report,
   //  and the URL that the seller wanted fetched for that.
   //
+  // `errors` are various error messages to be used for debugging. These are too
+  // sensitive for the renderers to see.
+  //
   // TODO(mmenke):  May be good to make some way to share worklets between
   // multiple auctions on the same page, but not auctions across pages.
   // Could create separate top level AuctionWorkletService objects (using the
@@ -119,5 +122,6 @@ interface AuctionWorkletService {
                 url.mojom.Origin winning_interest_group_owner,
                 string winning_interest_group_name,
                 WinningBidderReport bidder_report,
-                SellerReport seller_report);
+                SellerReport seller_report,
+                array<string> errors);
 };
diff --git a/content/services/auction_worklet/report_bindings.cc b/content/services/auction_worklet/report_bindings.cc
index 4f9f98c5c9670..d7c5ab0de2512 100644
--- a/content/services/auction_worklet/report_bindings.cc
+++ b/content/services/auction_worklet/report_bindings.cc
@@ -45,7 +45,7 @@ void ReportBindings::SendReportTo(
     bindings->report_url_ = GURL();
     args.GetIsolate()->ThrowException(
         v8::Exception::TypeError(v8_helper->CreateStringFromLiteral(
-            "SendReportTo requires 1 string parameter.")));
+            "sendReportTo requires 1 string parameter")));
     return;
   }
 
@@ -54,7 +54,8 @@ void ReportBindings::SendReportTo(
     bindings->report_url_ = GURL();
     args.GetIsolate()->ThrowException(
         v8::Exception::TypeError(v8_helper->CreateStringFromLiteral(
-            "SendReportTo may be called at most once.")));
+            "sendReportTo may be called at most once")));
+    return;
   }
 
   GURL url(url_string);
@@ -62,7 +63,7 @@ void ReportBindings::SendReportTo(
     bindings->exception_thrown_ = true;
     args.GetIsolate()->ThrowException(
         v8::Exception::TypeError(v8_helper->CreateStringFromLiteral(
-            "SendReportTo must be passed a valid HTTPS url.")));
+            "sendReportTo must be passed a valid HTTPS url")));
     return;
   }
 
diff --git a/content/services/auction_worklet/seller_worklet.cc b/content/services/auction_worklet/seller_worklet.cc
index fc36a677f9c47..750c27223af3e 100644
--- a/content/services/auction_worklet/seller_worklet.cc
+++ b/content/services/auction_worklet/seller_worklet.cc
@@ -13,6 +13,7 @@
 #include "base/callback.h"
 #include "base/logging.h"
 #include "base/stl_util.h"
+#include "base/strings/strcat.h"
 #include "base/time/time.h"
 #include "content/services/auction_worklet/auction_v8_helper.h"
 #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
@@ -114,6 +115,17 @@ SellerWorklet::ScoreResult::ScoreResult(double score)
   DCHECK_GT(score, 0);
 }
 
+SellerWorklet::ScoreResult::ScoreResult(base::Optional<std::string> error_msg)
+    : error_msg(std::move(error_msg)) {}
+
+SellerWorklet::ScoreResult::ScoreResult(const ScoreResult& other) = default;
+SellerWorklet::ScoreResult::ScoreResult(ScoreResult&& other) = default;
+SellerWorklet::ScoreResult::~ScoreResult() = default;
+SellerWorklet::ScoreResult& SellerWorklet::ScoreResult::operator=(
+    const ScoreResult&) = default;
+SellerWorklet::ScoreResult& SellerWorklet::ScoreResult::operator=(
+    ScoreResult&&) = default;
+
 SellerWorklet::Report::Report() = default;
 
 SellerWorklet::Report::Report(std::string signals_for_winner, GURL report_url)
@@ -121,12 +133,22 @@ SellerWorklet::Report::Report(std::string signals_for_winner, GURL report_url)
       signals_for_winner(std::move(signals_for_winner)),
       report_url(std::move(report_url)) {}
 
+SellerWorklet::Report::Report(base::Optional<std::string> error_msg)
+    : error_msg(std::move(error_msg)) {}
+
+SellerWorklet::Report::Report(const Report& other) = default;
+SellerWorklet::Report::Report(Report&& other) = default;
+SellerWorklet::Report::~Report() = default;
+SellerWorklet::Report& SellerWorklet::Report::operator=(const Report&) =
+    default;
+SellerWorklet::Report& SellerWorklet::Report::operator=(Report&&) = default;
+
 SellerWorklet::SellerWorklet(
     network::mojom::URLLoaderFactory* url_loader_factory,
     const GURL& script_source_url,
     AuctionV8Helper* v8_helper,
     LoadWorkletCallback load_worklet_callback)
-    : v8_helper_(v8_helper) {
+    : script_source_url_(script_source_url), v8_helper_(v8_helper) {
   DCHECK(load_worklet_callback);
   worklet_loader_ = std::make_unique<WorkletLoader>(
       url_loader_factory, script_source_url, v8_helper,
@@ -181,14 +203,22 @@ SellerWorklet::ScoreResult SellerWorklet::ScoreAd(
 
   v8::Local<v8::Value> score_ad_result;
   double score;
+  base::Optional<std::string> error_msg_out;
   if (!v8_helper_
-           ->RunScript(context, worklet_script_->Get(isolate), "scoreAd", args)
-           .ToLocal(&score_ad_result) ||
-      !gin::ConvertFromV8(isolate, score_ad_result, &score)) {
-    return ScoreResult();
+           ->RunScript(context, worklet_script_->Get(isolate), "scoreAd", args,
+                       error_msg_out)
+           .ToLocal(&score_ad_result)) {
+    return ScoreResult(std::move(error_msg_out));
   }
 
-  if (score <= 0 || std::isnan(score) || !std::isfinite(score))
+  if (!gin::ConvertFromV8(isolate, score_ad_result, &score) ||
+      std::isnan(score) || !std::isfinite(score)) {
+    return ScoreResult(
+        base::StrCat({script_source_url_.spec(),
+                      " scoreAd() did not return a valid number."}));
+  }
+
+  if (score <= 0)
     return ScoreResult();
 
   return ScoreResult(score);
@@ -236,11 +266,12 @@ SellerWorklet::Report SellerWorklet::ReportResult(
   args.push_back(browser_signals);
 
   v8::Local<v8::Value> signals_for_winner_value;
+  base::Optional<std::string> error_msg_out;
   if (!v8_helper_
            ->RunScript(context, worklet_script_->Get(isolate), "reportResult",
-                       args)
+                       args, error_msg_out)
            .ToLocal(&signals_for_winner_value)) {
-    return Report();
+    return Report(std::move(error_msg_out));
   }
 
   // Consider lack of error but no return value type, or a return value that
@@ -256,10 +287,12 @@ SellerWorklet::Report SellerWorklet::ReportResult(
 
 void SellerWorklet::OnDownloadComplete(
     LoadWorkletCallback load_worklet_callback,
-    std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script) {
+    std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
+    base::Optional<std::string> error_msg) {
   worklet_loader_.reset();
   worklet_script_ = std::move(worklet_script);
-  std::move(load_worklet_callback).Run(worklet_script_ != nullptr);
+  std::move(load_worklet_callback)
+      .Run(worklet_script_ != nullptr, std::move(error_msg));
 }
 
 }  // namespace auction_worklet
diff --git a/content/services/auction_worklet/seller_worklet.h b/content/services/auction_worklet/seller_worklet.h
index 3ce787c941f7f..cb1043935531c 100644
--- a/content/services/auction_worklet/seller_worklet.h
+++ b/content/services/auction_worklet/seller_worklet.h
@@ -9,6 +9,7 @@
 #include <string>
 
 #include "base/callback.h"
+#include "base/optional.h"
 #include "base/time/time.h"
 #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom-forward.h"
 #include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
@@ -27,8 +28,7 @@ class WorkletLoader;
 // seller worklet's Javascript.
 class SellerWorklet {
  public:
-  // The result of invoking scoreAd(). This could just be a double, but planning
-  // to add error information down the line.
+  // The result of invoking scoreAd().
   struct ScoreResult {
     // Creates a ScoreResult for a failure or rejected bid.
     ScoreResult();
@@ -36,15 +36,28 @@ class SellerWorklet {
     // Creates a ScoreResult for a successfully scored ad. `score` will be > 0.
     explicit ScoreResult(double score);
 
+    // Creates a ScoreResult representing a fatal error, potentially with a
+    // helpful diagnostic message in `error_msg`.
+    explicit ScoreResult(base::Optional<std::string> error_msg);
+
+    ScoreResult(const ScoreResult& other);
+    ScoreResult(ScoreResult&& other);
+
+    ~ScoreResult();
+
+    ScoreResult& operator=(const ScoreResult&);
+    ScoreResult& operator=(ScoreResult&&);
+
     // `success` will be false on any type of failure, and score will be 0.
-    //
-    // TODO(mmenke): Pass along some sort of error string instead, to make
-    // debugging easier.
     bool success = false;
 
     // Score. 0 if no bid is offered (even if underlying script returned a
     // negative value).
     double score = 0;
+
+    // Error message for debugging. This is not guaranteed to be present on
+    // failure.
+    base::Optional<std::string> error_msg;
   };
 
   // The result of invoking reportResult().
@@ -55,11 +68,20 @@ class SellerWorklet {
     // Creates a Report for a successful call.
     Report(std::string signals_for_winner, GURL report_url);
 
+    // Creates a ScoreResult representing a fatal error, potentially with a
+    // helpful diagnostic message in `error_msg`.
+    explicit Report(base::Optional<std::string> error_msg);
+
+    Report(const Report& other);
+    Report(Report&& other);
+
+    ~Report();
+
+    Report& operator=(const Report&);
+    Report& operator=(Report&&);
+
     // `success` will be false on any type of failure, including lack of a
     // method.
-    //
-    // TODO(mmenke): Pass along some sort of error string instead, to make
-    // debugging easier.
     bool success = false;
 
     // JSON data as a string. Sent to the winner's ReportWin function. JSON
@@ -69,9 +91,15 @@ class SellerWorklet {
     // Report URL, if one is provided. Empty on failure, or if no report URL is
     // provided.
     GURL report_url;
+
+    // Error message for debugging. This isn't guaranteed to have a value for
+    // all failures.
+    base::Optional<std::string> error_msg;
   };
 
-  using LoadWorkletCallback = base::OnceCallback<void(bool success)>;
+  using LoadWorkletCallback =
+      base::OnceCallback<void(bool success,
+                              base::Optional<std::string> error_msg)>;
 
   // Starts loading the worklet script on construction. Callback will be invoked
   // asynchronously once the data has been fetched or an error has occurred.
@@ -109,8 +137,10 @@ class SellerWorklet {
  private:
   void OnDownloadComplete(
       LoadWorkletCallback load_worklet_callback,
-      std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script);
+      std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
+      base::Optional<std::string> error_msg);
 
+  const GURL script_source_url_;
   AuctionV8Helper* const v8_helper_;
   std::unique_ptr<WorkletLoader> worklet_loader_;
 
diff --git a/content/services/auction_worklet/seller_worklet_unittest.cc b/content/services/auction_worklet/seller_worklet_unittest.cc
index 58c411e9ca247..bdaca9acadd5c 100644
--- a/content/services/auction_worklet/seller_worklet_unittest.cc
+++ b/content/services/auction_worklet/seller_worklet_unittest.cc
@@ -18,9 +18,13 @@
 #include "content/services/auction_worklet/worklet_test_util.h"
 #include "net/http/http_status_code.h"
 #include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock-matchers.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 
+using testing::HasSubstr;
+using testing::StartsWith;
+
 namespace auction_worklet {
 namespace {
 
@@ -85,23 +89,29 @@ class SellerWorkletTest : public testing::Test {
   // return line, expecting the provided result.
   void RunScoreAdWithReturnValueExpectingResult(
       const std::string& raw_return_value,
-      double expected_score) {
+      double expected_score,
+      base::Optional<std::string> expected_error_msg = base::nullopt) {
     RunScoreAdWithJavascriptExpectingResult(
-        CreateScoreAdScript(raw_return_value), expected_score);
+        CreateScoreAdScript(raw_return_value), expected_score,
+        std::move(expected_error_msg));
   }
 
   // Configures `url_loader_factory_` to return the provided script, and then
   // runs its generate_bid() function. Then runs the script, expecting the
   // provided result.
-  void RunScoreAdWithJavascriptExpectingResult(const std::string& javascript,
-                                               double expected_score) {
+  void RunScoreAdWithJavascriptExpectingResult(
+      const std::string& javascript,
+      double expected_score,
+      base::Optional<std::string> expected_error_msg = base::nullopt) {
     SCOPED_TRACE(javascript);
     AddJavascriptResponse(&url_loader_factory_, url_, javascript);
-    RunScoreAdExpectingResult(expected_score);
+    RunScoreAdExpectingResult(expected_score, std::move(expected_error_msg));
   }
 
   // Loads and runs a scode_ad() script, expecting the supplied result.
-  void RunScoreAdExpectingResult(double expected_score) {
+  void RunScoreAdExpectingResult(
+      double expected_score,
+      base::Optional<std::string> expected_error_msg = base::nullopt) {
     auto seller_worket = CreateWorklet();
     ASSERT_TRUE(seller_worket);
 
@@ -113,6 +123,10 @@ class SellerWorkletTest : public testing::Test {
                                browser_signal_bidding_duration_);
     EXPECT_EQ(expected_score > 0, actual_result.success);
     EXPECT_EQ(expected_score, actual_result.score);
+    EXPECT_EQ(expected_error_msg.has_value(),
+              actual_result.error_msg.has_value());
+    EXPECT_EQ(expected_error_msg.value_or("Not an error"),
+              actual_result.error_msg.value_or("Not an error"));
   }
 
   // Configures `url_loader_factory_` to return a report_result() script created
@@ -152,6 +166,10 @@ class SellerWorkletTest : public testing::Test {
     EXPECT_EQ(expected_report.signals_for_winner,
               actual_result.signals_for_winner);
     EXPECT_EQ(expected_report.report_url, actual_result.report_url);
+    EXPECT_EQ(expected_report.error_msg.has_value(),
+              actual_result.error_msg.has_value());
+    EXPECT_EQ(expected_report.error_msg.value_or("Not an error"),
+              actual_result.error_msg.value_or("Not an error"));
   }
 
   // Create a SellerWorklet, waiting for the URLLoader to complete. Returns
@@ -160,7 +178,7 @@ class SellerWorkletTest : public testing::Test {
     CHECK(!load_script_run_loop_);
 
     create_worklet_succeeded_ = false;
-    auto bidder_worket = std::make_unique<SellerWorklet>(
+    auto seller_worket = std::make_unique<SellerWorklet>(
         &url_loader_factory_, url_, &v8_helper_,
         base::BindOnce(&SellerWorkletTest::CreateWorkletCallback,
                        base::Unretained(this)));
@@ -169,15 +187,21 @@ class SellerWorkletTest : public testing::Test {
     load_script_run_loop_.reset();
     if (!create_worklet_succeeded_)
       return nullptr;
-    return bidder_worket;
+    return seller_worket;
   }
 
  protected:
-  void CreateWorkletCallback(bool success) {
+  void CreateWorkletCallback(bool success,
+                             base::Optional<std::string> error_msg) {
     create_worklet_succeeded_ = success;
+    error_msg_ = std::move(error_msg);
+    if (success)
+      EXPECT_FALSE(error_msg_.has_value());
     load_script_run_loop_->Quit();
   }
 
+  std::string last_error_msg() { return error_msg_.value_or("Not an error"); }
+
   base::test::TaskEnvironment task_environment_;
 
   const GURL url_ = GURL("https://url.test/");
@@ -201,6 +225,7 @@ class SellerWorkletTest : public testing::Test {
   // synchronously.
   std::unique_ptr<base::RunLoop> load_script_run_loop_;
   bool create_worklet_succeeded_ = false;
+  base::Optional<std::string> error_msg_;
 
   network::TestURLLoaderFactory url_loader_factory_;
   AuctionV8Helper v8_helper_;
@@ -210,11 +235,15 @@ TEST_F(SellerWorkletTest, NetworkError) {
   url_loader_factory_.AddResponse(url_.spec(), CreateBasicSellAdScript(),
                                   net::HTTP_NOT_FOUND);
   EXPECT_FALSE(CreateWorklet());
+  EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.",
+            last_error_msg());
 }
 
 TEST_F(SellerWorkletTest, CompileError) {
   AddJavascriptResponse(&url_loader_factory_, url_, "Invalid Javascript");
   EXPECT_FALSE(CreateWorklet());
+  EXPECT_THAT(last_error_msg(), StartsWith("https://url.test/:1 "));
+  EXPECT_THAT(last_error_msg(), HasSubstr("SyntaxError"));
 }
 
 // Test parsing of return values.
@@ -229,21 +258,31 @@ TEST_F(SellerWorkletTest, ScoreAd) {
   RunScoreAdWithReturnValueExpectingResult("-10", 0);
 
   // No return value.
-  RunScoreAdWithReturnValueExpectingResult("", 0);
+  RunScoreAdWithReturnValueExpectingResult(
+      "", 0, "https://url.test/ scoreAd() did not return a valid number.");
 
   // Wrong return type / invalid values.
-  RunScoreAdWithReturnValueExpectingResult("[15]", 0);
-  RunScoreAdWithReturnValueExpectingResult("1/0", 0);
-  RunScoreAdWithReturnValueExpectingResult("0/0", 0);
-  RunScoreAdWithReturnValueExpectingResult("-1/0", 0);
-  RunScoreAdWithReturnValueExpectingResult("true", 0);
+  RunScoreAdWithReturnValueExpectingResult(
+      "[15]", 0, "https://url.test/ scoreAd() did not return a valid number.");
+  RunScoreAdWithReturnValueExpectingResult(
+      "1/0", 0, "https://url.test/ scoreAd() did not return a valid number.");
+  RunScoreAdWithReturnValueExpectingResult(
+      "0/0", 0, "https://url.test/ scoreAd() did not return a valid number.");
+  RunScoreAdWithReturnValueExpectingResult(
+      "-1/0", 0, "https://url.test/ scoreAd() did not return a valid number.");
+  RunScoreAdWithReturnValueExpectingResult(
+      "true", 0, "https://url.test/ scoreAd() did not return a valid number.");
 
   // Throw exception.
-  RunScoreAdWithReturnValueExpectingResult("shrimp", 0);
+  RunScoreAdWithReturnValueExpectingResult(
+      "shrimp", 0,
+      "https://url.test/:4 Uncaught ReferenceError: shrimp is not defined.");
 }
 
 TEST_F(SellerWorkletTest, ScoreAdDateNotAvailable) {
-  RunScoreAdWithReturnValueExpectingResult("Date.parse(Date().toString())", 0);
+  RunScoreAdWithReturnValueExpectingResult(
+      "Date.parse(Date().toString())", 0,
+      "https://url.test/:4 Uncaught ReferenceError: Date is not defined.");
 }
 
 // Checks that input parameters are correctly passed in.
@@ -363,7 +402,9 @@ TEST_F(SellerWorkletTest, ReportResult) {
 
   // Throw exception.
   RunReportResultCreatedScriptExpectingResult(
-      "shrimp", std::string() /* extra_code */, SellerWorklet::Report());
+      "shrimp", std::string() /* extra_code */,
+      SellerWorklet::Report("https://url.test/:4 Uncaught ReferenceError: "
+                            "shrimp is not defined."));
 }
 
 // Tests reporting URLs.
@@ -377,29 +418,49 @@ TEST_F(SellerWorkletTest, ReportResultSendReportTo) {
 
   // Disallowed schemes.
   RunReportResultCreatedScriptExpectingResult(
-      "1", R"(sendReportTo("http://foo.test/"))", SellerWorklet::Report());
+      "1", R"(sendReportTo("http://foo.test/"))",
+      SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
+                            "sendReportTo must be passed a valid HTTPS url."));
   RunReportResultCreatedScriptExpectingResult(
-      "1", R"(sendReportTo("file:///foo/"))", SellerWorklet::Report());
+      "1", R"(sendReportTo("file:///foo/"))",
+      SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
+                            "sendReportTo must be passed a valid HTTPS url."));
 
   // Multiple calls.
   RunReportResultCreatedScriptExpectingResult(
       "1",
       R"(sendReportTo("https://foo.test/"); sendReportTo("https://foo.test/"))",
-      SellerWorklet::Report());
+      SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
+                            "sendReportTo may be called at most once."));
+
+  // No message if caught, but still no URL.
+  RunReportResultCreatedScriptExpectingResult(
+      "1",
+      R"(try {
+        sendReportTo("https://foo.test/");
+        sendReportTo("https://foo.test/")} catch(e) {})",
+      SellerWorklet::Report("1", GURL()));
 
   // Not a URL.
-  RunReportResultCreatedScriptExpectingResult("1", R"(sendReportTo("France"))",
-                                              SellerWorklet::Report());
-  RunReportResultCreatedScriptExpectingResult("1", R"(sendReportTo(null))",
-                                              SellerWorklet::Report());
-  RunReportResultCreatedScriptExpectingResult("1", R"(sendReportTo([5]))",
-                                              SellerWorklet::Report());
+  RunReportResultCreatedScriptExpectingResult(
+      "1", R"(sendReportTo("France"))",
+      SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
+                            "sendReportTo must be passed a valid HTTPS url."));
+  RunReportResultCreatedScriptExpectingResult(
+      "1", R"(sendReportTo(null))",
+      SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
+                            "sendReportTo requires 1 string parameter."));
+  RunReportResultCreatedScriptExpectingResult(
+      "1", R"(sendReportTo([5]))",
+      SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
+                            "sendReportTo requires 1 string parameter."));
 }
 
 TEST_F(SellerWorkletTest, ReportResultDateNotAvailable) {
   RunReportResultCreatedScriptExpectingResult(
       "1", R"(sendReportTo("https://foo.test/" + Date().toString()))",
-      SellerWorklet::Report());
+      SellerWorklet::Report(
+          "https://url.test/:3 Uncaught ReferenceError: Date is not defined."));
 }
 
 TEST_F(SellerWorkletTest, ReportResultParameters) {
diff --git a/content/services/auction_worklet/trusted_bidding_signals.cc b/content/services/auction_worklet/trusted_bidding_signals.cc
index f9da2b48edb66..894c91d727820 100644
--- a/content/services/auction_worklet/trusted_bidding_signals.cc
+++ b/content/services/auction_worklet/trusted_bidding_signals.cc
@@ -11,6 +11,7 @@
 #include "base/bind.h"
 #include "base/callback.h"
 #include "base/logging.h"
+#include "base/strings/strcat.h"
 #include "content/services/auction_worklet/auction_downloader.h"
 #include "content/services/auction_worklet/auction_v8_helper.h"
 #include "gin/converter.h"
@@ -28,7 +29,8 @@ TrustedBiddingSignals::TrustedBiddingSignals(
     const GURL& trusted_bidding_signals_url,
     AuctionV8Helper* v8_helper,
     LoadSignalsCallback load_signals_callback)
-    : v8_helper_(v8_helper),
+    : trusted_bidding_signals_url_(trusted_bidding_signals_url),
+      v8_helper_(v8_helper),
       load_signals_callback_(std::move(load_signals_callback)) {
   DCHECK(!trusted_bidding_signals_keys.empty());
   DCHECK(load_signals_callback_);
@@ -81,14 +83,17 @@ v8::Local<v8::Object> TrustedBiddingSignals::GetSignals(
 
 void TrustedBiddingSignals::OnDownloadComplete(
     std::vector<std::string> trusted_bidding_signals_keys,
-    std::unique_ptr<std::string> body) {
+    std::unique_ptr<std::string> body,
+    base::Optional<std::string> error_msg) {
   auction_downloader_.reset();
 
   if (!body) {
-    std::move(load_signals_callback_).Run(false);
+    std::move(load_signals_callback_).Run(false, std::move(error_msg));
     return;
   }
 
+  DCHECK(!error_msg.has_value());
+
   AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper_);
   v8::Context::Scope context_scope(v8_helper_->scratch_context());
 
@@ -96,7 +101,9 @@ void TrustedBiddingSignals::OnDownloadComplete(
   if (!v8_helper_->CreateValueFromJson(v8_helper_->scratch_context(), *body)
            .ToLocal(&v8_data) ||
       !v8_data->IsObject()) {
-    std::move(load_signals_callback_).Run(false);
+    std::string error = base::StrCat({trusted_bidding_signals_url_.spec(),
+                                      " Unable to parse as a JSON object."});
+    std::move(load_signals_callback_).Run(false, std::move(error));
     return;
   }
 
@@ -108,7 +115,7 @@ void TrustedBiddingSignals::OnDownloadComplete(
     v8::Local<v8::Value> v8_string_value;
     std::string value;
     if (!v8_helper_->CreateUtf8String(key).ToLocal(&v8_key)) {
-      std::move(load_signals_callback_).Run(false);
+      std::move(load_signals_callback_).Run(false, base::nullopt);
       return;
     }
     // Only the `has_result` check should be able to fail.
@@ -124,7 +131,7 @@ void TrustedBiddingSignals::OnDownloadComplete(
     }
     json_data_[key] = std::move(value);
   }
-  std::move(load_signals_callback_).Run(true);
+  std::move(load_signals_callback_).Run(true, base::nullopt);
 }
 
 }  // namespace auction_worklet
diff --git a/content/services/auction_worklet/trusted_bidding_signals.h b/content/services/auction_worklet/trusted_bidding_signals.h
index 59e7b2a6b99d5..f0adee6b9fee8 100644
--- a/content/services/auction_worklet/trusted_bidding_signals.h
+++ b/content/services/auction_worklet/trusted_bidding_signals.h
@@ -11,6 +11,7 @@
 #include <vector>
 
 #include "base/callback.h"
+#include "base/optional.h"
 #include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
 #include "url/gurl.h"
 #include "v8/include/v8.h"
@@ -33,7 +34,9 @@ class AuctionV8Helper;
 // clone operation as a copy).
 class TrustedBiddingSignals {
  public:
-  using LoadSignalsCallback = base::OnceCallback<void(bool success)>;
+  using LoadSignalsCallback =
+      base::OnceCallback<void(bool success,
+                              base::Optional<std::string> error_msg)>;
 
   // Starts loading the JSON data on construction. `trusted_bidding_signals_url`
   // must be the base URL (no query params added). Callback will be invoked
@@ -62,8 +65,10 @@ class TrustedBiddingSignals {
 
  private:
   void OnDownloadComplete(std::vector<std::string> trusted_bidding_signals_keys,
-                          std::unique_ptr<std::string> body);
+                          std::unique_ptr<std::string> body,
+                          base::Optional<std::string> error_msg);
 
+  const GURL trusted_bidding_signals_url_;  // original, for error messages.
   AuctionV8Helper* const v8_helper_;
 
   LoadSignalsCallback load_signals_callback_;
diff --git a/content/services/auction_worklet/trusted_bidding_signals_unittest.cc b/content/services/auction_worklet/trusted_bidding_signals_unittest.cc
index 5273f466e3512..984857683c442 100644
--- a/content/services/auction_worklet/trusted_bidding_signals_unittest.cc
+++ b/content/services/auction_worklet/trusted_bidding_signals_unittest.cc
@@ -98,8 +98,11 @@ class TrustedBiddingSignalsTest : public testing::Test {
   }
 
  protected:
-  void LoadSignalsCallback(bool success) {
+  void LoadSignalsCallback(bool success,
+                           base::Optional<std::string> error_msg) {
     load_signals_succeeded_ = success;
+    error_msg_ = std::move(error_msg);
+    EXPECT_EQ(load_signals_succeeded_, !error_msg_.has_value());
     load_signals_run_loop_->Quit();
   }
 
@@ -113,6 +116,7 @@ class TrustedBiddingSignalsTest : public testing::Test {
   // synchronously.
   std::unique_ptr<base::RunLoop> load_signals_run_loop_;
   bool load_signals_succeeded_ = false;
+  base::Optional<std::string> error_msg_;
 
   network::TestURLLoaderFactory url_loader_factory_;
   AuctionV8Helper v8_helper_;
@@ -123,18 +127,29 @@ TEST_F(TrustedBiddingSignalsTest, NetworkError) {
       "https://url.test/?hostname=publisher&keys=key1", kBaseJson,
       net::HTTP_NOT_FOUND);
   EXPECT_FALSE(FetchBiddingSignals({"key1"}, kHostname));
+  ASSERT_TRUE(error_msg_.has_value());
+  EXPECT_EQ(
+      "Failed to load https://url.test/?hostname=publisher&keys=key1 "
+      "HTTP status = 404 Not Found.",
+      error_msg_.value());
 }
 
 TEST_F(TrustedBiddingSignalsTest, ResponseNotJson) {
   EXPECT_FALSE(FetchBiddingSignalsWithResponse(
       GURL("https://url.test/?hostname=publisher&keys=key1"), "Not Json",
       {"key1"}, kHostname));
+  ASSERT_TRUE(error_msg_.has_value());
+  EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.",
+            error_msg_.value());
 }
 
 TEST_F(TrustedBiddingSignalsTest, ResponseNotObject) {
   EXPECT_FALSE(FetchBiddingSignalsWithResponse(
       GURL("https://url.test/?hostname=publisher&keys=key1"), "42", {"key1"},
       kHostname));
+  ASSERT_TRUE(error_msg_.has_value());
+  EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.",
+            error_msg_.value());
 }
 
 TEST_F(TrustedBiddingSignalsTest, KeyMissing) {
diff --git a/content/services/auction_worklet/worklet_loader.cc b/content/services/auction_worklet/worklet_loader.cc
index 689372ea6d04c..b2aa1ee4bff3b 100644
--- a/content/services/auction_worklet/worklet_loader.cc
+++ b/content/services/auction_worklet/worklet_loader.cc
@@ -38,17 +38,20 @@ WorkletLoader::WorkletLoader(
 
 WorkletLoader::~WorkletLoader() = default;
 
-void WorkletLoader::OnDownloadComplete(std::unique_ptr<std::string> body) {
+void WorkletLoader::OnDownloadComplete(std::unique_ptr<std::string> body,
+                                       base::Optional<std::string> error_msg) {
   DCHECK(load_worklet_callback_);
 
   auction_downloader_.reset();
 
   if (!body) {
-    std::move(load_worklet_callback_).Run(nullptr /* worklet_script */);
+    std::move(load_worklet_callback_)
+        .Run(nullptr /* worklet_script */, std::move(error_msg));
     return;
   }
 
   std::unique_ptr<v8::Global<v8::UnboundScript>> global_script;
+  DCHECK(!error_msg.has_value());
 
   // Need to release the isolate and context before invoking the callback, in
   // case the `v8_helper_` is destroyed.
@@ -57,13 +60,15 @@ void WorkletLoader::OnDownloadComplete(std::unique_ptr<std::string> body) {
     v8::Context::Scope context_scope(v8_helper_->scratch_context());
 
     v8::Local<v8::UnboundScript> local_script;
-    if (v8_helper_->Compile(*body, script_source_url_).ToLocal(&local_script)) {
+    if (v8_helper_->Compile(*body, script_source_url_, error_msg)
+            .ToLocal(&local_script)) {
       global_script = std::make_unique<v8::Global<v8::UnboundScript>>(
           v8_helper_->isolate(), local_script);
     }
   }
 
-  std::move(load_worklet_callback_).Run(std::move(global_script));
+  std::move(load_worklet_callback_)
+      .Run(std::move(global_script), std::move(error_msg));
 }
 
 }  // namespace auction_worklet
diff --git a/content/services/auction_worklet/worklet_loader.h b/content/services/auction_worklet/worklet_loader.h
index 47136682ed154..dfa081773f0b9 100644
--- a/content/services/auction_worklet/worklet_loader.h
+++ b/content/services/auction_worklet/worklet_loader.h
@@ -10,6 +10,7 @@
 #include <vector>
 
 #include "base/callback.h"
+#include "base/optional.h"
 #include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
 #include "url/gurl.h"
 #include "v8/include/v8.h"
@@ -25,11 +26,9 @@ class WorkletLoader {
   // On success, `worklet_script` is compiled script, not bound to any context.
   // It can be repeatedly bound to different contexts and executed, without
   // persisting any state.
-  //
-  // TODO(mmenke): Pass along some sort of error string on failure, to make
-  // debugging easier.
   using LoadWorkletCallback = base::OnceCallback<void(
-      std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script)>;
+      std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
+      base::Optional<std::string> error_msg)>;
 
   // Starts loading the worklet script on construction. Callback will be invoked
   // asynchronously once the data has been fetched or an error has occurred.
@@ -43,7 +42,8 @@ class WorkletLoader {
   ~WorkletLoader();
 
  private:
-  void OnDownloadComplete(std::unique_ptr<std::string> body);
+  void OnDownloadComplete(std::unique_ptr<std::string> body,
+                          base::Optional<std::string> error_msg);
 
   const GURL script_source_url_;
   AuctionV8Helper* const v8_helper_;
diff --git a/content/services/auction_worklet/worklet_loader_unittest.cc b/content/services/auction_worklet/worklet_loader_unittest.cc
index b42a666c2f072..25e1c6718720a 100644
--- a/content/services/auction_worklet/worklet_loader_unittest.cc
+++ b/content/services/auction_worklet/worklet_loader_unittest.cc
@@ -16,10 +16,14 @@
 #include "content/services/auction_worklet/worklet_test_util.h"
 #include "net/http/http_status_code.h"
 #include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock-matchers.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 #include "v8/include/v8.h"
 
+using testing::HasSubstr;
+using testing::StartsWith;
+
 namespace auction_worklet {
 namespace {
 
@@ -34,11 +38,18 @@ class WorkletLoaderTest : public testing::Test {
   ~WorkletLoaderTest() override = default;
 
   void LoadWorkletCallback(
-      std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script) {
+      std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
+      base::Optional<std::string> error_msg) {
     load_succeeded_ = !!worklet_script;
+    error_msg_ = std::move(error_msg);
+    EXPECT_EQ(load_succeeded_, !error_msg_.has_value());
     run_loop_.Quit();
   }
 
+  std::string last_error_msg() const {
+    return error_msg_.value_or("Not an error");
+  }
+
  protected:
   base::test::TaskEnvironment task_environment_;
 
@@ -47,6 +58,7 @@ class WorkletLoaderTest : public testing::Test {
   GURL url_ = GURL("https://foo.test/");
   base::RunLoop run_loop_;
   bool load_succeeded_ = false;
+  base::Optional<std::string> error_msg_;
 };
 
 TEST_F(WorkletLoaderTest, NetworkError) {
@@ -59,6 +71,8 @@ TEST_F(WorkletLoaderTest, NetworkError) {
                      base::Unretained(this)));
   run_loop_.Run();
   EXPECT_FALSE(load_succeeded_);
+  EXPECT_EQ("Failed to load https://foo.test/ HTTP status = 404 Not Found.",
+            last_error_msg());
 }
 
 TEST_F(WorkletLoaderTest, CompileError) {
@@ -69,6 +83,8 @@ TEST_F(WorkletLoaderTest, CompileError) {
                      base::Unretained(this)));
   run_loop_.Run();
   EXPECT_FALSE(load_succeeded_);
+  EXPECT_THAT(last_error_msg(), StartsWith("https://foo.test/:1 "));
+  EXPECT_THAT(last_error_msg(), HasSubstr("SyntaxError"));
 }
 
 TEST_F(WorkletLoaderTest, Success) {
@@ -92,9 +108,10 @@ TEST_F(WorkletLoaderTest, DeleteDuringCallbackSuccess) {
       std::make_unique<WorkletLoader>(
           &url_loader_factory_, url_, v8_helper.get(),
           base::BindLambdaForTesting(
-              [&](std::unique_ptr<v8::Global<v8::UnboundScript>>
-                      worklet_script) {
+              [&](std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
+                  base::Optional<std::string> error_msg) {
                 EXPECT_TRUE(worklet_script);
+                EXPECT_FALSE(error_msg.has_value());
                 worklet_script.reset();
                 worklet_loader.reset();
                 v8_helper.reset();
@@ -114,9 +131,13 @@ TEST_F(WorkletLoaderTest, DeleteDuringCallbackCompileError) {
       std::make_unique<WorkletLoader>(
           &url_loader_factory_, url_, v8_helper.get(),
           base::BindLambdaForTesting(
-              [&](std::unique_ptr<v8::Global<v8::UnboundScript>>
-                      worklet_script) {
+              [&](std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
+                  base::Optional<std::string> error_msg) {
                 EXPECT_FALSE(worklet_script);
+                ASSERT_TRUE(error_msg.has_value());
+                EXPECT_THAT(error_msg.value(),
+                            StartsWith("https://foo.test/:1 "));
+                EXPECT_THAT(error_msg.value(), HasSubstr("SyntaxError"));
                 worklet_loader.reset();
                 v8_helper.reset();
                 run_loop.Quit();