[Lldb-commits] [lldb] [lldb/Interpreter] Surface Python exceptions from scripted extensions (PR #198153)

Med Ismail Bennani via lldb-commits lldb-commits at lists.llvm.org
Sun May 17 09:39:54 PDT 2026


https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/198153

>From d81d7248a35166813ba87b9d6ce62c9e7d6976ed Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Sun, 17 May 2026 09:39:36 -0700
Subject: [PATCH] [lldb/Interpreter] Surface Python exceptions from scripted
 extensions

When calling scripted affordance methods, Python exceptions were silently
consumed by llvm::consumeError(), making it very hard to debug scripted
extensions: users would see a generic "Failed to create script object."
or just a null result, with no clue about what raised in their script.

This patch propagates the actual Python error through the existing
error-reporting channel of each call site:

- The Dispatch and CallStaticMethod templates now extract the full Python
  backtrace via PythonException::ReadBacktrace and populate the Status
  out-parameter with a message of the form "Python exception in
  <script-class> method '<method>':\n<traceback>".

- ErrorWithMessage no longer overwrites a Status that already has detailed
  content, so the traceback survives the helper's existing logging.

- ScriptedInterface now holds the ScriptedMetadata (set by each concrete
  CreatePluginObject) so the error message can name the script class.

- For entry points that have no return-channel for errors
  (ScriptedProcess::CreateInstance, ScriptedThreadPlan::DidPush,
  BreakpointResolverScripted, OperatingSystemPython's constructor) the
  detailed error is broadcast via Debugger::ReportError so users see
  it instead of a silent failure.

- For entry points that already return llvm::Expected<T> or Status
  (ScriptedThread::Create, ScriptedFrameProvider::CreateInstance,
  StopHookScripted::SetScriptCallback) the existing return type now
  carries the detailed error, replacing the previous consumeError +
  generic-message pattern.

- ScriptedProcess::DoAttach / DoReadMemory / DoWriteMemory wrap their
  affordance Status with the operation that failed.

A new test directory at lldb/test/API/functionalities/scripted_extensions/
listens for eBroadcastBitError diagnostics and asserts that each
malformed scripted extension produces a user-visible error.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
 .../Interfaces/ScriptedBreakpointInterface.h  |   2 +-
 .../ScriptedFrameProviderInterface.h          |   2 +-
 .../Interfaces/ScriptedInterface.h            |  43 +++-
 .../Interfaces/ScriptedStopHookInterface.h    |   2 +-
 .../Interfaces/ScriptedThreadPlanInterface.h  |   2 +-
 .../lldb/Interpreter/ScriptInterpreter.h      |   4 +
 lldb/include/lldb/Utility/ScriptedMetadata.h  |   2 +-
 .../Breakpoint/BreakpointResolverScripted.cpp |   8 +-
 .../Python/OperatingSystemPython.cpp          |   8 +-
 .../Process/scripted/ScriptedProcess.cpp      |  66 +++++-
 .../Process/scripted/ScriptedProcess.h        |  14 +-
 .../Process/scripted/ScriptedThread.cpp       |   7 +-
 .../OperatingSystemPythonInterface.cpp        |  31 ++-
 .../ScriptedBreakpointPythonInterface.cpp     |  33 ++-
 .../ScriptedFrameProviderPythonInterface.cpp  |  30 ++-
 .../ScriptedFramePythonInterface.cpp          |  60 +++++-
 .../ScriptedPlatformPythonInterface.cpp       |  16 +-
 .../ScriptedProcessPythonInterface.cpp        |  83 ++++++--
 .../Interfaces/ScriptedPythonInterface.h      | 128 +++++++++++-
 .../ScriptedStopHookPythonInterface.cpp       |  13 +-
 .../ScriptedThreadPlanPythonInterface.cpp     |  33 ++-
 .../ScriptedThreadPythonInterface.cpp         |  59 +++++-
 lldb/source/Target/ScriptedThreadPlan.cpp     |   5 +
 .../scripted_extensions/Makefile              |   3 +
 .../TestScriptedExtensionsDiagnostics.py      | 124 +++++++++++
 .../scripted_extensions/main.c                |   1 +
 .../malformed_scripted_extensions.py          | 195 ++++++++++++++++++
 27 files changed, 897 insertions(+), 77 deletions(-)
 create mode 100644 lldb/test/API/functionalities/scripted_extensions/Makefile
 create mode 100644 lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py
 create mode 100644 lldb/test/API/functionalities/scripted_extensions/main.c
 create mode 100644 lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py

diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
index a169432d2759a..1cfc6728258b9 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
@@ -15,7 +15,7 @@
 #include "lldb/lldb-private.h"
 
 namespace lldb_private {
-class ScriptedBreakpointInterface : public ScriptedInterface {
+class ScriptedBreakpointInterface : virtual public ScriptedInterface {
 public:
   virtual llvm::Expected<StructuredData::GenericSP>
   CreatePluginObject(llvm::StringRef class_name, lldb::BreakpointSP break_sp,
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
index b04af0c817b6e..2f149b6e85857 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
@@ -14,7 +14,7 @@
 #include "ScriptedInterface.h"
 
 namespace lldb_private {
-class ScriptedFrameProviderInterface : public ScriptedInterface {
+class ScriptedFrameProviderInterface : virtual public ScriptedInterface {
 public:
   virtual bool AppliesToThread(llvm::StringRef class_name,
                                lldb::ThreadSP thread_sp) {
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
index 8ace90927b582..4f5686833932a 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
@@ -14,11 +14,13 @@
 #include "lldb/Core/StructuredDataImpl.h"
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
+#include "lldb/Utility/ScriptedMetadata.h"
 #include "lldb/Utility/UnimplementedError.h"
 #include "lldb/lldb-private.h"
 
 #include "llvm/Support/Compiler.h"
 
+#include <functional>
 #include <string>
 
 namespace lldb_private {
@@ -31,6 +33,23 @@ class ScriptedInterface {
     return m_object_instance_sp;
   }
 
+  /// Set error callback to surface Python exceptions directly to users.
+  ///
+  /// This allows command handlers to receive Python exception details
+  /// immediately rather than relying on diagnostic broadcasts.
+  ///
+  /// \param callback Function to call with Status containing exception details.
+  virtual void SetErrorCallback(std::function<void(const Status &)> callback) {}
+
+  /// Clear the error callback.
+  virtual void ClearErrorCallback() {}
+
+  llvm::StringRef GetScriptClassName() const {
+    if (m_scripted_metadata_sp)
+      return m_scripted_metadata_sp->GetClassName();
+    return {};
+  }
+
   struct AbstractMethodRequirement {
     llvm::StringLiteral name;
     size_t min_arg_count = 0;
@@ -56,18 +75,20 @@ class ScriptedInterface {
   static Ret ErrorWithMessage(llvm::StringRef caller_name,
                               llvm::StringRef error_msg, Status &error,
                               LLDBLog log_category = LLDBLog::Process) {
+    // Log the error for debugging (includes function signature for context).
     LLDB_LOGF(GetLog(log_category), "%s ERROR = %s", caller_name.data(),
               error_msg.data());
-    std::string full_error_message =
-        llvm::Twine(caller_name + llvm::Twine(" ERROR = ") +
-                    llvm::Twine(error_msg))
-            .str();
-    if (const char *detailed_error = error.AsCString())
-      full_error_message +=
-          llvm::Twine(llvm::Twine(" (") + llvm::Twine(detailed_error) +
-                      llvm::Twine(")"))
-              .str();
-    error = Status(std::move(full_error_message));
+
+    // For user-facing messages, just pass through the Status if it already
+    // has detailed information (like Python tracebacks); otherwise set it.
+    const char *existing_error = error.AsCString();
+    if (!error.Fail() || !existing_error || existing_error[0] == '\0') {
+      // Status is empty, populate it with the simple error message.
+      error = Status::FromErrorString(error_msg.data());
+    }
+    // If Status already has content, leave it as-is (it has the Python
+    // traceback).
+
     return {};
   }
 
@@ -96,6 +117,8 @@ class ScriptedInterface {
 
 protected:
   StructuredData::GenericSP m_object_instance_sp;
+  lldb::ScriptedMetadataSP m_scripted_metadata_sp = nullptr;
 };
 } // namespace lldb_private
+
 #endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h
index a829e62351fe8..73262bac909d1 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h
@@ -14,7 +14,7 @@
 #include "ScriptedInterface.h"
 
 namespace lldb_private {
-class ScriptedStopHookInterface : public ScriptedInterface {
+class ScriptedStopHookInterface : virtual public ScriptedInterface {
 public:
   virtual llvm::Expected<StructuredData::GenericSP>
   CreatePluginObject(llvm::StringRef class_name, lldb::TargetSP target_sp,
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedThreadPlanInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedThreadPlanInterface.h
index ee634d15f2a9e..c773d6ef12832 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedThreadPlanInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedThreadPlanInterface.h
@@ -14,7 +14,7 @@
 #include "ScriptedInterface.h"
 
 namespace lldb_private {
-class ScriptedThreadPlanInterface : public ScriptedInterface {
+class ScriptedThreadPlanInterface : virtual public ScriptedInterface {
 public:
   virtual llvm::Expected<StructuredData::GenericSP>
   CreatePluginObject(llvm::StringRef class_name,
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 58af82fb48390..2cc3890d86916 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -663,6 +663,10 @@ class ScriptInterpreter : public PluginInterface {
 
   lldb::TargetSP GetOpaqueTypeFromSBTarget(const lldb::SBTarget &target) const;
 
+  /// Get the debugger associated with this script interpreter.
+  Debugger &GetDebugger() { return m_debugger; }
+  const Debugger &GetDebugger() const { return m_debugger; }
+
 protected:
   Debugger &m_debugger;
   lldb::ScriptLanguage m_script_lang;
diff --git a/lldb/include/lldb/Utility/ScriptedMetadata.h b/lldb/include/lldb/Utility/ScriptedMetadata.h
index c6b660830e158..0d9b3909249ee 100644
--- a/lldb/include/lldb/Utility/ScriptedMetadata.h
+++ b/lldb/include/lldb/Utility/ScriptedMetadata.h
@@ -19,7 +19,7 @@ class ScriptedMetadata {
 public:
   ScriptedMetadata(llvm::StringRef class_name,
                    StructuredData::DictionarySP dict_sp)
-      : m_class_name(class_name.data()), m_args_sp(dict_sp) {}
+      : m_class_name(class_name.str()), m_args_sp(dict_sp) {}
 
   ScriptedMetadata(const ProcessInfo &process_info) {
     lldb::ScriptedMetadataSP metadata_sp = process_info.GetScriptedMetadata();
diff --git a/lldb/source/Breakpoint/BreakpointResolverScripted.cpp b/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
index 84d918029faf8..e23bd978c5a4e 100644
--- a/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
+++ b/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
@@ -20,6 +20,7 @@
 #include "lldb/Target/Target.h"
 #include "lldb/Utility/Log.h"
 #include "lldb/Utility/StreamString.h"
+#include "llvm/Support/FormatVariadic.h"
 
 using namespace lldb;
 using namespace lldb_private;
@@ -88,7 +89,12 @@ void BreakpointResolverScripted::CreateImplementationIfNeeded(
       m_interface_sp->CreatePluginObject(m_class_name, breakpoint_sp, m_args);
   if (!obj_or_err) {
     m_interface_sp.reset();
-    m_error = Status::FromError(obj_or_err.takeError());
+    std::string msg = llvm::toString(obj_or_err.takeError());
+    Debugger::ReportError(
+        llvm::formatv("Failed to create BreakpointResolverScripted: {0}", msg)
+            .str(),
+        target.GetDebugger().GetID());
+    m_error = Status(msg);
     return;
   }
   StructuredData::ObjectSP object_sp = *obj_or_err;
diff --git a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
index 96b2b9d9ee088..aedea7f886571 100644
--- a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
+++ b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
@@ -33,6 +33,7 @@
 #include "lldb/Utility/StreamString.h"
 #include "lldb/Utility/StructuredData.h"
 #include "lldb/ValueObject/ValueObjectVariable.h"
+#include "llvm/Support/FormatVariadic.h"
 
 #include <memory>
 
@@ -120,7 +121,12 @@ OperatingSystemPython::OperatingSystemPython(lldb_private::Process *process,
       os_plugin_class_name, exe_ctx, nullptr);
 
   if (!obj_or_err) {
-    llvm::consumeError(obj_or_err.takeError());
+    std::string msg = llvm::toString(obj_or_err.takeError());
+    if (process)
+      Debugger::ReportError(
+          llvm::formatv("Failed to create OperatingSystemPython: {0}", msg)
+              .str(),
+          process->GetTarget().GetDebugger().GetID());
     return;
   }
 
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
index c254a6841b707..5470936d6051d 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
@@ -27,6 +27,10 @@
 
 #include "Plugins/ObjectFile/Placeholder/ObjectFilePlaceholder.h"
 
+#include "llvm/Support/Error.h"
+
+#include <string>
+
 using namespace lldb;
 using namespace lldb_private;
 
@@ -57,11 +61,27 @@ lldb::ProcessSP ScriptedProcess::CreateInstance(lldb::TargetSP target_sp,
 
   ScriptedMetadata scripted_metadata(target_sp->GetProcessLaunchInfo());
 
+  // CreateInstance is invoked for every process plugin during process
+  // creation; if the launch info doesn't request a scripted process, bail
+  // out silently rather than treating the missing class name as an error.
+  if (!scripted_metadata)
+    return nullptr;
+
   Status error;
   auto process_sp = std::shared_ptr<ScriptedProcess>(
       new ScriptedProcess(target_sp, listener_sp, scripted_metadata, error));
 
   if (error.Fail() || !process_sp || !process_sp->m_interface_up) {
+    // CreateInstance returns nullptr on failure with no Status output
+    // parameter, so we must report the error via the diagnostic system for
+    // users to see it.
+    if (error.Fail()) {
+      Debugger::ReportError(
+          llvm::formatv("Failed to create ScriptedProcess: {0}",
+                        error.AsCString())
+              .str(),
+          target_sp->GetDebugger().GetID());
+    }
     LLDB_LOGF(GetLog(LLDBLog::Process), "%s", error.AsCString());
     return nullptr;
   }
@@ -113,8 +133,10 @@ ScriptedProcess::ScriptedProcess(lldb::TargetSP target_sp,
       m_scripted_metadata.GetArgsSP());
 
   if (!obj_or_err) {
-    llvm::consumeError(obj_or_err.takeError());
-    error = Status::FromErrorString("Failed to create script object.");
+    // Extract the detailed error message including the Python backtrace.
+    std::string error_msg = llvm::toString(obj_or_err.takeError());
+    error = Status::FromErrorStringWithFormatv(
+        "Failed to create script object: {0}", error_msg);
     return;
   }
 
@@ -181,8 +203,10 @@ void ScriptedProcess::DidResume() {
 Status ScriptedProcess::DoResume(RunDirection direction) {
   LLDB_LOGF(GetLog(LLDBLog::Process), "ScriptedProcess::%s resuming process", __FUNCTION__);
 
-  if (direction == RunDirection::eRunForward)
-    return GetInterface().Resume();
+  if (direction == RunDirection::eRunForward) {
+    Status error = GetInterface().Resume();
+    return error;
+  }
   // FIXME: Pipe reverse continue through Scripted Processes
   return Status::FromErrorStringWithFormatv(
       "{0} does not support reverse execution of processes", GetPluginName());
@@ -190,10 +214,13 @@ Status ScriptedProcess::DoResume(RunDirection direction) {
 
 Status ScriptedProcess::DoAttach(const ProcessAttachInfo &attach_info) {
   Status error = GetInterface().Attach(attach_info);
+  if (error.Fail()) {
+    error = Status::FromErrorStringWithFormatv(
+        "Failed to attach to scripted process: {0}", error.AsCString());
+    return error;
+  }
   SetPrivateState(eStateRunning);
   SetPrivateState(eStateStopped);
-  if (error.Fail())
-    return error;
   // NOTE: We need to set the PID before finishing to attach otherwise we will
   // hit an assert when calling the attach completion handler.
   DidLaunch();
@@ -225,8 +252,14 @@ size_t ScriptedProcess::DoReadMemory(lldb::addr_t addr, void *buf, size_t size,
   lldb::DataExtractorSP data_extractor_sp =
       GetInterface().ReadMemoryAtAddress(addr, size, error);
 
-  if (!data_extractor_sp || !data_extractor_sp->HasData() || error.Fail())
+  if (!data_extractor_sp || !data_extractor_sp->HasData() || error.Fail()) {
+    if (error.Fail()) {
+      error = Status::FromErrorStringWithFormatv(
+          "Failed to read memory from scripted process at 0x{0:x}: {1}", addr,
+          error.AsCString());
+    }
     return 0;
+  }
 
   offset_t bytes_copied = data_extractor_sp->CopyByteOrderedData(
       0, data_extractor_sp->GetByteSize(), buf, size, GetByteOrder());
@@ -252,9 +285,15 @@ size_t ScriptedProcess::DoWriteMemory(lldb::addr_t vm_addr, const void *buf,
   lldb::offset_t bytes_written =
       GetInterface().WriteMemoryAtAddress(vm_addr, data_extractor_sp, error);
 
-  if (!bytes_written || bytes_written == LLDB_INVALID_OFFSET)
+  if (!bytes_written || bytes_written == LLDB_INVALID_OFFSET) {
+    if (error.Fail()) {
+      error = Status::FromErrorStringWithFormatv(
+          "Failed to write memory to scripted process at 0x{0:x}: {1}", vm_addr,
+          error.AsCString());
+    }
     return ScriptedInterface::ErrorWithMessage<size_t>(
         LLVM_PRETTY_FUNCTION, "Failed to copy write buffer to memory.", error);
+  }
 
   // FIXME: We should use the diagnostic system to report a warning if the
   // `bytes_written` is different from `size`.
@@ -566,3 +605,14 @@ void *ScriptedProcess::GetImplementation() {
     return object_instance_sp->GetAsGeneric()->GetValue();
   return nullptr;
 }
+
+void ScriptedProcess::SetScriptedInterfaceErrorCallback(
+    std::function<void(const Status &)> callback) {
+  if (m_interface_up)
+    m_interface_up->SetErrorCallback(std::move(callback));
+}
+
+void ScriptedProcess::ClearScriptedInterfaceErrorCallback() {
+  if (m_interface_up)
+    m_interface_up->ClearErrorCallback();
+}
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
index 8371180734217..9ce4a3f8d3cc9 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
+++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
@@ -17,8 +17,6 @@
 
 #include "ScriptedThread.h"
 
-#include <mutex>
-
 namespace lldb_private {
 class ScriptedProcess : public Process {
 public:
@@ -93,6 +91,18 @@ class ScriptedProcess : public Process {
 
   void *GetImplementation() override;
 
+  /// Set error callback to surface Python exceptions directly to users.
+  ///
+  /// This allows command handlers to receive Python exception details
+  /// immediately rather than relying on diagnostic broadcasts.
+  ///
+  /// \param callback Function to call with Status containing exception details.
+  void SetScriptedInterfaceErrorCallback(
+      std::function<void(const Status &)> callback);
+
+  /// Clear the error callback.
+  void ClearScriptedInterfaceErrorCallback();
+
   void ForceScriptedState(lldb::StateType state) override {
     // If we're about to stop, we should fetch the loaded dynamic libraries
     // dictionary before emitting the private stop event to avoid having the
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
index 77c418f5a11df..3f6a09f2cbf8a 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
@@ -61,11 +61,8 @@ ScriptedThread::Create(ScriptedProcess &process,
       thread_class_name, exe_ctx, process.m_scripted_metadata.GetArgsSP(),
       script_object);
 
-  if (!obj_or_err) {
-    llvm::consumeError(obj_or_err.takeError());
-    return llvm::createStringError(llvm::inconvertibleErrorCode(),
-                                   "Failed to create script object.");
-  }
+  if (!obj_or_err)
+    return obj_or_err.takeError();
 
   StructuredData::GenericSP owned_script_object_sp = *obj_or_err;
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/OperatingSystemPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/OperatingSystemPythonInterface.cpp
index a48a9bb8fa292..8d45a590c65a4 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/OperatingSystemPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/OperatingSystemPythonInterface.cpp
@@ -30,8 +30,12 @@ llvm::Expected<StructuredData::GenericSP>
 OperatingSystemPythonInterface::CreatePluginObject(
     llvm::StringRef class_name, ExecutionContext &exe_ctx,
     StructuredData::DictionarySP args_sp, StructuredData::Generic *script_obj) {
-  return ScriptedPythonInterface::CreatePluginObject(class_name, nullptr,
-                                                     exe_ctx.GetProcessSP());
+  auto obj_or_err = ScriptedPythonInterface::CreatePluginObject(
+      class_name, nullptr, exe_ctx.GetProcessSP());
+  if (obj_or_err)
+    m_scripted_metadata_sp =
+        std::make_shared<ScriptedMetadata>(class_name, nullptr);
+  return obj_or_err;
 }
 
 StructuredData::DictionarySP
@@ -41,6 +45,11 @@ OperatingSystemPythonInterface::CreateThread(lldb::tid_t tid,
   StructuredData::DictionarySP dict = Dispatch<StructuredData::DictionarySP>(
       "create_thread", error, tid, context);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "CreateThread Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, dict,
                                                     error))
     return {};
@@ -53,6 +62,11 @@ StructuredData::ArraySP OperatingSystemPythonInterface::GetThreadInfo() {
   StructuredData::ArraySP arr =
       Dispatch<StructuredData::ArraySP>("get_thread_info", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetThreadInfo Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, arr,
                                                     error))
     return {};
@@ -69,6 +83,12 @@ OperatingSystemPythonInterface::GetRegisterContextForTID(lldb::tid_t tid) {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_register_data", error, tid);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script),
+             "GetRegisterContextForTID Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
@@ -79,6 +99,13 @@ OperatingSystemPythonInterface::GetRegisterContextForTID(lldb::tid_t tid) {
 std::optional<bool> OperatingSystemPythonInterface::DoesPluginReportAllThreads() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("does_plugin_report_all_threads", error);
+
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script),
+             "DoesPluginReportAllThreads Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedBreakpointPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedBreakpointPythonInterface.cpp
index 76e7a8e5338c8..c2bdbb8963273 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedBreakpointPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedBreakpointPythonInterface.cpp
@@ -32,8 +32,12 @@ llvm::Expected<StructuredData::GenericSP>
 ScriptedBreakpointPythonInterface::CreatePluginObject(
     llvm::StringRef class_name, lldb::BreakpointSP break_sp,
     const StructuredDataImpl &args_sp) {
-  return ScriptedPythonInterface::CreatePluginObject(class_name, nullptr,
-                                                     break_sp, args_sp);
+  auto obj_or_err = ScriptedPythonInterface::CreatePluginObject(
+      class_name, nullptr, break_sp, args_sp);
+  if (obj_or_err)
+    m_scripted_metadata_sp =
+        std::make_shared<ScriptedMetadata>(class_name, nullptr);
+  return obj_or_err;
 }
 
 bool ScriptedBreakpointPythonInterface::OverridesResolver(
@@ -71,6 +75,11 @@ bool ScriptedBreakpointPythonInterface::ResolverCallback(
 
   StructuredData::ObjectSP obj = Dispatch("__callback__", error, sym_ctx);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "ResolverCallback Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error)) {
     Log *log = GetLog(LLDBLog::Script);
@@ -84,6 +93,11 @@ lldb::SearchDepth ScriptedBreakpointPythonInterface::GetDepth() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("__get_depth__", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetDepth Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error)) {
     return lldb::eSearchDepthModule;
@@ -100,6 +114,11 @@ std::optional<std::string> ScriptedBreakpointPythonInterface::GetShortHelp() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_short_help", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetShortHelp Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error)) {
     return {};
@@ -114,8 +133,11 @@ lldb::BreakpointLocationSP ScriptedBreakpointPythonInterface::WasHit(
   lldb::BreakpointLocationSP loc_sp = Dispatch<lldb::BreakpointLocationSP>(
       "was_hit", py_error, frame_sp, bp_loc_sp);
 
-  if (py_error.Fail())
+  if (py_error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "WasHit Python exception: {0}",
+             py_error.AsCString());
     return bp_loc_sp;
+  }
 
   return loc_sp;
 }
@@ -127,6 +149,11 @@ ScriptedBreakpointPythonInterface::GetLocationDescription(
   StructuredData::ObjectSP obj =
       Dispatch("get_location_description", error, bp_loc_sp, level);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script),
+             "GetLocationDescription Python exception: {0}", error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp
index 8c95592dfc5f0..bd44a1b739649 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp
@@ -35,6 +35,12 @@ bool ScriptedFrameProviderPythonInterface::AppliesToThread(
   Status error;
   StructuredData::ObjectSP obj =
       CallStaticMethod(class_name, "applies_to_thread", error, thread_sp);
+
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "AppliesToThread Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return fail_value;
@@ -50,8 +56,12 @@ ScriptedFrameProviderPythonInterface::CreatePluginObject(
     return llvm::createStringError("invalid frame list");
 
   StructuredDataImpl sd_impl(args_sp);
-  return ScriptedPythonInterface::CreatePluginObject(class_name, nullptr,
-                                                     input_frames, sd_impl);
+  auto obj_or_err = ScriptedPythonInterface::CreatePluginObject(
+      class_name, nullptr, input_frames, sd_impl);
+  if (obj_or_err)
+    m_scripted_metadata_sp =
+        std::make_shared<ScriptedMetadata>(class_name, args_sp);
+  return obj_or_err;
 }
 
 std::string ScriptedFrameProviderPythonInterface::GetDescription(
@@ -59,6 +69,12 @@ std::string ScriptedFrameProviderPythonInterface::GetDescription(
   Status error;
   StructuredData::ObjectSP obj =
       CallStaticMethod(class_name, "get_description", error);
+
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetDescription Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
@@ -72,6 +88,11 @@ ScriptedFrameProviderPythonInterface::GetPriority(llvm::StringRef class_name) {
   StructuredData::ObjectSP obj =
       CallStaticMethod(class_name, "get_priority", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetPriority Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return std::nullopt;
@@ -89,6 +110,11 @@ ScriptedFrameProviderPythonInterface::GetFrameAtIndex(uint32_t index) {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_frame_at_index", error, index);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetFrameAtIndex Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.cpp
index eaee6a51992c6..62b541f328267 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.cpp
@@ -34,14 +34,23 @@ ScriptedFramePythonInterface::CreatePluginObject(
   ExecutionContextRefSP exe_ctx_ref_sp =
       std::make_shared<ExecutionContextRef>(exe_ctx);
   StructuredDataImpl sd_impl(args_sp);
-  return ScriptedPythonInterface::CreatePluginObject(class_name, script_obj,
-                                                     exe_ctx_ref_sp, sd_impl);
+  auto obj_or_err = ScriptedPythonInterface::CreatePluginObject(
+      class_name, script_obj, exe_ctx_ref_sp, sd_impl);
+  if (obj_or_err)
+    m_scripted_metadata_sp =
+        std::make_shared<ScriptedMetadata>(class_name, args_sp);
+  return obj_or_err;
 }
 
 lldb::user_id_t ScriptedFramePythonInterface::GetID() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_id", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetID Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return LLDB_INVALID_FRAME_ID;
@@ -53,6 +62,11 @@ lldb::addr_t ScriptedFramePythonInterface::GetPC() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_pc", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetPC Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return LLDB_INVALID_ADDRESS;
@@ -65,6 +79,8 @@ std::optional<SymbolContext> ScriptedFramePythonInterface::GetSymbolContext() {
   auto sym_ctx = Dispatch<SymbolContext>("get_symbol_context", error);
 
   if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetSymbolContext Python exception: {0}",
+             error.AsCString());
     return ErrorWithMessage<SymbolContext>(LLVM_PRETTY_FUNCTION,
                                            error.AsCString(), error);
   }
@@ -76,6 +92,11 @@ std::optional<std::string> ScriptedFramePythonInterface::GetFunctionName() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_function_name", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetFunctionName Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
@@ -88,6 +109,11 @@ ScriptedFramePythonInterface::GetDisplayFunctionName() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_display_function_name", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script),
+             "GetDisplayFunctionName Python exception: {0}", error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
@@ -99,6 +125,11 @@ bool ScriptedFramePythonInterface::IsInlined() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("is_inlined", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "IsInlined Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return false;
@@ -110,6 +141,11 @@ bool ScriptedFramePythonInterface::IsArtificial() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("is_artificial", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "IsArtificial Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return false;
@@ -121,6 +157,11 @@ bool ScriptedFramePythonInterface::IsHidden() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("is_hidden", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "IsHidden Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return false;
@@ -133,6 +174,11 @@ StructuredData::DictionarySP ScriptedFramePythonInterface::GetRegisterInfo() {
   StructuredData::DictionarySP dict =
       Dispatch<StructuredData::DictionarySP>("get_register_info", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetRegisterInfo Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, dict,
                                                     error))
     return {};
@@ -144,6 +190,11 @@ std::optional<std::string> ScriptedFramePythonInterface::GetRegisterContext() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_register_context", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script),
+             "GetRegisterContext Python exception: {0}", error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
@@ -156,6 +207,8 @@ lldb::ValueObjectListSP ScriptedFramePythonInterface::GetVariables() {
   auto vals = Dispatch<lldb::ValueObjectListSP>("get_variables", error);
 
   if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetVariables Python exception: {0}",
+             error.AsCString());
     return ErrorWithMessage<lldb::ValueObjectListSP>(LLVM_PRETTY_FUNCTION,
                                                      error.AsCString(), error);
   }
@@ -172,6 +225,9 @@ ScriptedFramePythonInterface::GetValueObjectForVariableExpression(
                                            status);
 
   if (dispatch_error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script),
+             "GetValueObjectForVariableExpression Python exception: {0}",
+             dispatch_error.AsCString());
     return ErrorWithMessage<lldb::ValueObjectSP>(
         LLVM_PRETTY_FUNCTION, dispatch_error.AsCString(), dispatch_error);
   }
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPlatformPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPlatformPythonInterface.cpp
index 76ae3d32033a9..c6662fac534d9 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPlatformPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPlatformPythonInterface.cpp
@@ -34,8 +34,12 @@ ScriptedPlatformPythonInterface::CreatePluginObject(
   ExecutionContextRefSP exe_ctx_ref_sp =
       std::make_shared<ExecutionContextRef>(exe_ctx);
   StructuredDataImpl sd_impl(args_sp);
-  return ScriptedPythonInterface::CreatePluginObject(class_name, script_obj,
-                                                     exe_ctx_ref_sp, sd_impl);
+  auto obj_or_err = ScriptedPythonInterface::CreatePluginObject(
+      class_name, script_obj, exe_ctx_ref_sp, sd_impl);
+  if (obj_or_err)
+    m_scripted_metadata_sp =
+        std::make_shared<ScriptedMetadata>(class_name, args_sp);
+  return obj_or_err;
 }
 
 StructuredData::DictionarySP ScriptedPlatformPythonInterface::ListProcesses() {
@@ -44,6 +48,10 @@ StructuredData::DictionarySP ScriptedPlatformPythonInterface::ListProcesses() {
       Dispatch<StructuredData::DictionarySP>("list_processes", error);
 
   if (!dict_sp || !dict_sp->IsValid() || error.Fail()) {
+    if (error.Fail()) {
+      LLDB_LOG(GetLog(LLDBLog::Script), "ListProcesses Python exception: {0}",
+               error.AsCString());
+    }
     return ScriptedInterface::ErrorWithMessage<StructuredData::DictionarySP>(
         LLVM_PRETTY_FUNCTION,
         llvm::Twine("Null or invalid object (" +
@@ -62,6 +70,10 @@ ScriptedPlatformPythonInterface::GetProcessInfo(lldb::pid_t pid) {
       Dispatch<StructuredData::DictionarySP>("get_process_info", error, pid);
 
   if (!dict_sp || !dict_sp->IsValid() || error.Fail()) {
+    if (error.Fail()) {
+      LLDB_LOG(GetLog(LLDBLog::Script), "GetProcessInfo Python exception: {0}",
+               error.AsCString());
+    }
     return ScriptedInterface::ErrorWithMessage<StructuredData::DictionarySP>(
         LLVM_PRETTY_FUNCTION,
         llvm::Twine("Null or invalid object (" +
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedProcessPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedProcessPythonInterface.cpp
index e57426cc90ba2..33dd2dab5e8a7 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedProcessPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedProcessPythonInterface.cpp
@@ -37,8 +37,12 @@ ScriptedProcessPythonInterface::CreatePluginObject(
   ExecutionContextRefSP exe_ctx_ref_sp =
       std::make_shared<ExecutionContextRef>(exe_ctx);
   StructuredDataImpl sd_impl(args_sp);
-  return ScriptedPythonInterface::CreatePluginObject(class_name, script_obj,
-                                                     exe_ctx_ref_sp, sd_impl);
+  auto obj_or_err = ScriptedPythonInterface::CreatePluginObject(
+      class_name, script_obj, exe_ctx_ref_sp, sd_impl);
+  if (obj_or_err)
+    m_scripted_metadata_sp =
+        std::make_shared<ScriptedMetadata>(class_name, args_sp);
+  return obj_or_err;
 }
 
 StructuredData::DictionarySP ScriptedProcessPythonInterface::GetCapabilities() {
@@ -46,6 +50,11 @@ StructuredData::DictionarySP ScriptedProcessPythonInterface::GetCapabilities() {
   StructuredData::DictionarySP dict =
       Dispatch<StructuredData::DictionarySP>("get_capabilities", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetCapabilities Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, dict,
                                                     error))
     return {};
@@ -88,6 +97,11 @@ StructuredData::DictionarySP ScriptedProcessPythonInterface::GetThreadsInfo() {
   StructuredData::DictionarySP dict =
       Dispatch<StructuredData::DictionarySP>("get_threads_info", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetThreadsInfo Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, dict,
                                                     error))
     return {};
@@ -97,48 +111,53 @@ StructuredData::DictionarySP ScriptedProcessPythonInterface::GetThreadsInfo() {
 
 bool ScriptedProcessPythonInterface::CreateBreakpoint(lldb::addr_t addr,
                                                       Status &error) {
-  Status py_error;
   StructuredData::ObjectSP obj =
-      Dispatch("create_breakpoint", py_error, addr, error);
+      Dispatch("create_breakpoint", error, addr, error);
 
-  // If there was an error on the python call, surface it to the user.
-  if (py_error.Fail())
-    error = std::move(py_error);
+  // Error is already set by Dispatch if a Python exception occurred.
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "CreateBreakpoint failed: {0}",
+             error.AsCString());
+    return false;
+  }
 
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
-    return {};
+    return false;
 
   return obj->GetBooleanValue();
 }
 
 lldb::DataExtractorSP ScriptedProcessPythonInterface::ReadMemoryAtAddress(
     lldb::addr_t address, size_t size, Status &error) {
-  Status py_error;
   lldb::DataExtractorSP data_sp = Dispatch<lldb::DataExtractorSP>(
-      "read_memory_at_address", py_error, address, size, error);
+      "read_memory_at_address", error, address, size, error);
 
-  // If there was an error on the python call, surface it to the user.
-  if (py_error.Fail())
-    error = std::move(py_error);
+  // Error is already set by Dispatch if a Python exception occurred.
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "ReadMemoryAtAddress failed: {0}",
+             error.AsCString());
+  }
 
   return data_sp;
 }
 
 lldb::offset_t ScriptedProcessPythonInterface::WriteMemoryAtAddress(
     lldb::addr_t addr, lldb::DataExtractorSP data_sp, Status &error) {
-  Status py_error;
   StructuredData::ObjectSP obj =
-      Dispatch("write_memory_at_address", py_error, addr, data_sp, error);
+      Dispatch("write_memory_at_address", error, addr, data_sp, error);
+
+  // Error is already set by Dispatch if a Python exception occurred.
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "WriteMemoryAtAddress failed: {0}",
+             error.AsCString());
+    return LLDB_INVALID_OFFSET;
+  }
 
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return LLDB_INVALID_OFFSET;
 
-  // If there was an error on the python call, surface it to the user.
-  if (py_error.Fail())
-    error = std::move(py_error);
-
   return obj->GetUnsignedIntegerValue(LLDB_INVALID_OFFSET);
 }
 
@@ -147,6 +166,11 @@ StructuredData::ArraySP ScriptedProcessPythonInterface::GetLoadedImages() {
   StructuredData::ArraySP array =
       Dispatch<StructuredData::ArraySP>("get_loaded_images", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetLoadedImages Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, array,
                                                     error))
     return {};
@@ -158,6 +182,11 @@ lldb::pid_t ScriptedProcessPythonInterface::GetProcessID() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_process_id", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetProcessID Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return LLDB_INVALID_PROCESS_ID;
@@ -169,6 +198,11 @@ bool ScriptedProcessPythonInterface::IsAlive() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("is_alive", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "IsAlive Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
@@ -181,6 +215,12 @@ ScriptedProcessPythonInterface::GetScriptedThreadPluginName() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_scripted_thread_plugin", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script),
+             "GetScriptedThreadPluginName Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
@@ -198,6 +238,11 @@ StructuredData::DictionarySP ScriptedProcessPythonInterface::GetMetadata() {
   StructuredData::DictionarySP dict =
       Dispatch<StructuredData::DictionarySP>("get_process_metadata", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetMetadata Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, dict,
                                                     error))
     return {};
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index 637ca1b2ab1f9..fbaec7249ccc6 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -9,12 +9,14 @@
 #ifndef LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDPYTHONINTERFACE_H
 #define LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDPYTHONINTERFACE_H
 
+#include <functional>
 #include <optional>
 #include <sstream>
 #include <tuple>
 #include <type_traits>
 #include <utility>
 
+#include "lldb/Core/Debugger.h"
 #include "lldb/Interpreter/Interfaces/ScriptedInterface.h"
 #include "lldb/Utility/DataBufferHeap.h"
 
@@ -29,6 +31,22 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
   ScriptedPythonInterface(ScriptInterpreterPythonImpl &interpreter);
   ~ScriptedPythonInterface() override = default;
 
+  /// Set callback to surface Python exceptions to CommandReturnObject.
+  ///
+  /// When set, this callback will be invoked whenever a Python exception occurs
+  /// in scripting affordance methods, allowing errors to be surfaced directly
+  /// to the user via CommandReturnObject::AppendError().
+  ///
+  /// If no callback is registered, errors will be reported via
+  /// Debugger::ReportError() instead.
+  ///
+  /// \param callback Function to call with Status containing exception details.
+  using ErrorCallback = std::function<void(const Status &)>;
+  void SetErrorCallback(ErrorCallback callback) override {
+    m_error_callback = std::move(callback);
+  }
+  void ClearErrorCallback() override { m_error_callback = nullptr; }
+
   enum class AbstractMethodCheckerCases {
     eNotImplemented,
     eNotAllocated,
@@ -130,7 +148,15 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
       llvm::Expected<PythonObject> callable_or_err =
           class_dict.GetItem(method_name);
       if (!callable_or_err) {
-        llvm::consumeError(callable_or_err.takeError());
+        Log *log = GetLog(LLDBLog::Script);
+        if (log) {
+          std::string error_msg =
+              ExtractPythonError(callable_or_err.takeError());
+          LLDB_LOGF(log, "Failed to get method '%s': %s", method_name.data(),
+                    error_msg.c_str());
+        } else {
+          llvm::consumeError(callable_or_err.takeError());
+        }
         SET_CASE_AND_CONTINUE(method_name,
                               AbstractMethodCheckerCases::eNotAllocated)
       }
@@ -145,7 +171,15 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
 
       auto arg_info_or_err = callable.GetArgInfo();
       if (!arg_info_or_err) {
-        llvm::consumeError(arg_info_or_err.takeError());
+        Log *log = GetLog(LLDBLog::Script);
+        if (log) {
+          std::string error_msg =
+              ExtractPythonError(arg_info_or_err.takeError());
+          LLDB_LOGF(log, "Failed to get arg info for method '%s': %s",
+                    method_name.data(), error_msg.c_str());
+        } else {
+          llvm::consumeError(arg_info_or_err.takeError());
+        }
         SET_CASE_AND_CONTINUE(method_name,
                               AbstractMethodCheckerCases::eUnknownArgumentCount)
       }
@@ -256,14 +290,18 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
 
         std::apply(
             [&init, &expected_return_object](auto &&...args) {
-              llvm::consumeError(expected_return_object.takeError());
+              // Consume placeholder error (expected initial state).
+              if (!expected_return_object)
+                llvm::consumeError(expected_return_object.takeError());
               expected_return_object = init(args...);
             },
             std::tuple_cat(transformed_args, std::make_tuple(dict)));
       } else {
         std::apply(
             [&init, &expected_return_object](auto &&...args) {
-              llvm::consumeError(expected_return_object.takeError());
+              // Consume placeholder error (expected initial state).
+              if (!expected_return_object)
+                llvm::consumeError(expected_return_object.takeError());
               expected_return_object = init(args...);
             },
             transformed_args);
@@ -465,13 +503,39 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
         llvm::createStringError("not initialized");
     std::apply(
         [&method, &expected_return_object](auto &&...args) {
-          llvm::consumeError(expected_return_object.takeError());
+          // Consume placeholder error (expected initial state).
+          if (!expected_return_object)
+            llvm::consumeError(expected_return_object.takeError());
           expected_return_object = method(args...);
         },
         transformed_args);
 
     if (llvm::Error e = expected_return_object.takeError()) {
-      error = Status::FromError(std::move(e));
+      // Extract Python backtrace and log it.
+      std::string detailed_error = ExtractPythonError(std::move(e));
+
+      Log *log = GetLog(LLDBLog::Script);
+      if (log) {
+        LLDB_LOGF(log, "%s: Python exception in static method %s:\n%s",
+                  caller_signature.c_str(), method_name.data(),
+                  detailed_error.c_str());
+      }
+
+      // Create Status with full context including the interface type.
+      // TODO: Stringify `args` and include them in the message so users
+      // can see what was passed to the failing call (e.g.
+      // `read_memory_at_address(0x500000000, 4)`). Requires a SFINAE
+      // helper that falls back to a placeholder for types without a
+      // format_provider / operator<<.
+      error = Status::FromErrorStringWithFormatv(
+          "Python exception in {0} method '{1}':\n{2}", GetScriptClassName(),
+          method_name, detailed_error);
+
+      // Surface error to user: use callback if available.
+      if (m_error_callback) {
+        m_error_callback(error);
+      }
+
       return ErrorWithMessage<T>(
           caller_signature, "python static method could not be called", error);
     }
@@ -490,6 +554,23 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
   }
 
 protected:
+  /// Extract detailed error message including Python backtrace if available.
+  ///
+  /// This helper processes llvm::Error objects that may contain PythonException
+  /// instances, extracting full Python backtraces when available.
+  ///
+  /// \param error The llvm::Error to extract information from.
+  /// \return A string containing the error message, including full Python
+  ///         backtrace if the error was a PythonException.
+  static std::string ExtractPythonError(llvm::Error error) {
+    std::string error_msg;
+    llvm::handleAllErrors(
+        std::move(error),
+        [&](python::PythonException &E) { error_msg = E.ReadBacktrace(); },
+        [&](const llvm::ErrorInfoBase &E) { error_msg = E.message(); });
+    return error_msg;
+  }
+
   template <typename T = StructuredData::ObjectSP>
   T ExtractValueFromPythonObject(python::PythonObject &p, Status &error) {
     return p.CreateStructuredObject();
@@ -528,14 +609,40 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
         llvm::createStringError("not initialized");
     std::apply(
         [&implementor, &method_name, &expected_return_object](auto &&...args) {
-          llvm::consumeError(expected_return_object.takeError());
+          // Consume placeholder error (expected initial state).
+          if (!expected_return_object)
+            llvm::consumeError(expected_return_object.takeError());
           expected_return_object =
               implementor.CallMethod(method_name.data(), args...);
         },
         transformed_args);
 
     if (llvm::Error e = expected_return_object.takeError()) {
-      error = Status::FromError(std::move(e));
+      // Extract Python backtrace and log it.
+      std::string detailed_error = ExtractPythonError(std::move(e));
+
+      Log *log = GetLog(LLDBLog::Script);
+      if (log) {
+        LLDB_LOGF(log, "%s: Python exception in %s:\n%s",
+                  caller_signature.c_str(), method_name.data(),
+                  detailed_error.c_str());
+      }
+
+      // Create Status with full context including the interface type.
+      // TODO: Stringify `args` and include them in the message so users
+      // can see what was passed to the failing call (e.g.
+      // `read_memory_at_address(0x500000000, 4)`). Requires a SFINAE
+      // helper that falls back to a placeholder for types without a
+      // format_provider / operator<<.
+      error = Status::FromErrorStringWithFormatv(
+          "Python exception in {0} method '{1}':\n{2}", GetScriptClassName(),
+          method_name, detailed_error);
+
+      // Surface error to user: use callback if available.
+      if (m_error_callback) {
+        m_error_callback(error);
+      }
+
       return ErrorWithMessage<T>(caller_signature,
                                  "python method could not be called", error);
     }
@@ -730,6 +837,11 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
 
   // The lifetime is managed by the ScriptInterpreter
   ScriptInterpreterPythonImpl &m_interpreter;
+
+  // Optional callback for surfacing errors to users (e.g.,
+  // CommandReturnObject). If not set, errors are reported via
+  // Debugger::ReportError().
+  ErrorCallback m_error_callback;
 };
 
 template <>
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStopHookPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStopHookPythonInterface.cpp
index d497fc8a15721..5171ab5213713 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStopHookPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStopHookPythonInterface.cpp
@@ -29,8 +29,12 @@ llvm::Expected<StructuredData::GenericSP>
 ScriptedStopHookPythonInterface::CreatePluginObject(llvm::StringRef class_name,
                                                     lldb::TargetSP target_sp,
                                                     const StructuredDataImpl &args_sp) {
-  return ScriptedPythonInterface::CreatePluginObject(class_name, nullptr,
-                                                     target_sp, args_sp);
+  auto obj_or_err = ScriptedPythonInterface::CreatePluginObject(
+      class_name, nullptr, target_sp, args_sp);
+  if (obj_or_err)
+    m_scripted_metadata_sp =
+        std::make_shared<ScriptedMetadata>(class_name, nullptr);
+  return obj_or_err;
 }
 
 llvm::Expected<bool>
@@ -41,6 +45,11 @@ ScriptedStopHookPythonInterface::HandleStop(ExecutionContext &exe_ctx,
   Status error;
   StructuredData::ObjectSP obj = Dispatch("handle_stop", error, exe_ctx_ref_sp, output_sp);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "HandleStop Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error)) {
     if (!obj)
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedThreadPlanPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedThreadPlanPythonInterface.cpp
index 95789bfdc39aa..8df055eae6285 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedThreadPlanPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedThreadPlanPythonInterface.cpp
@@ -28,8 +28,12 @@ llvm::Expected<StructuredData::GenericSP>
 ScriptedThreadPlanPythonInterface::CreatePluginObject(
     const llvm::StringRef class_name, lldb::ThreadPlanSP thread_plan_sp,
     const StructuredDataImpl &args_sp) {
-  return ScriptedPythonInterface::CreatePluginObject(class_name, nullptr,
-                                                     thread_plan_sp, args_sp);
+  auto obj_or_err = ScriptedPythonInterface::CreatePluginObject(
+      class_name, nullptr, thread_plan_sp, args_sp);
+  if (obj_or_err)
+    m_scripted_metadata_sp =
+        std::make_shared<ScriptedMetadata>(class_name, nullptr);
+  return obj_or_err;
 }
 
 llvm::Expected<bool>
@@ -37,6 +41,11 @@ ScriptedThreadPlanPythonInterface::ExplainsStop(Event *event) {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("explains_stop", error, event);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "ExplainsStop Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error)) {
     if (!obj)
@@ -52,6 +61,11 @@ ScriptedThreadPlanPythonInterface::ShouldStop(Event *event) {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("should_stop", error, event);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "ShouldStop Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error)) {
     if (!obj)
@@ -66,6 +80,11 @@ llvm::Expected<bool> ScriptedThreadPlanPythonInterface::IsStale() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("is_stale", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "IsStale Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error)) {
     if (!obj)
@@ -80,6 +99,11 @@ lldb::StateType ScriptedThreadPlanPythonInterface::GetRunState() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("should_step", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetRunState Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return lldb::eStateStepping;
@@ -93,8 +117,11 @@ ScriptedThreadPlanPythonInterface::GetStopDescription(lldb::StreamSP &stream) {
   Status error;
   Dispatch("stop_description", error, stream);
 
-  if (error.Fail())
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script),
+             "GetStopDescription Python exception: {0}", error.AsCString());
     return error.ToError();
+  }
 
   return llvm::Error::success();
 }
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedThreadPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedThreadPythonInterface.cpp
index c34c6ee74fbb5..5a384a2e07374 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedThreadPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedThreadPythonInterface.cpp
@@ -34,14 +34,23 @@ ScriptedThreadPythonInterface::CreatePluginObject(
   ExecutionContextRefSP exe_ctx_ref_sp =
       std::make_shared<ExecutionContextRef>(exe_ctx);
   StructuredDataImpl sd_impl(args_sp);
-  return ScriptedPythonInterface::CreatePluginObject(class_name, script_obj,
-                                                     exe_ctx_ref_sp, sd_impl);
+  auto obj_or_err = ScriptedPythonInterface::CreatePluginObject(
+      class_name, script_obj, exe_ctx_ref_sp, sd_impl);
+  if (obj_or_err)
+    m_scripted_metadata_sp =
+        std::make_shared<ScriptedMetadata>(class_name, args_sp);
+  return obj_or_err;
 }
 
 lldb::tid_t ScriptedThreadPythonInterface::GetThreadID() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_thread_id", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetThreadID Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return LLDB_INVALID_THREAD_ID;
@@ -53,6 +62,11 @@ std::optional<std::string> ScriptedThreadPythonInterface::GetName() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_name", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetName Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
@@ -64,6 +78,11 @@ lldb::StateType ScriptedThreadPythonInterface::GetState() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_state", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetState Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return eStateInvalid;
@@ -75,6 +94,11 @@ std::optional<std::string> ScriptedThreadPythonInterface::GetQueue() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_queue", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetQueue Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
@@ -87,6 +111,11 @@ StructuredData::DictionarySP ScriptedThreadPythonInterface::GetStopReason() {
   StructuredData::DictionarySP dict =
       Dispatch<StructuredData::DictionarySP>("get_stop_reason", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetStopReason Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, dict,
                                                     error))
     return {};
@@ -99,6 +128,11 @@ StructuredData::ArraySP ScriptedThreadPythonInterface::GetStackFrames() {
   StructuredData::ArraySP arr =
       Dispatch<StructuredData::ArraySP>("get_stackframes", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetStackFrames Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, arr,
                                                     error))
     return {};
@@ -111,6 +145,11 @@ StructuredData::DictionarySP ScriptedThreadPythonInterface::GetRegisterInfo() {
   StructuredData::DictionarySP dict =
       Dispatch<StructuredData::DictionarySP>("get_register_info", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetRegisterInfo Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, dict,
                                                     error))
     return {};
@@ -122,6 +161,11 @@ std::optional<std::string> ScriptedThreadPythonInterface::GetRegisterContext() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_register_context", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script),
+             "GetRegisterContext Python exception: {0}", error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
@@ -134,6 +178,11 @@ StructuredData::ArraySP ScriptedThreadPythonInterface::GetExtendedInfo() {
   StructuredData::ArraySP arr =
       Dispatch<StructuredData::ArraySP>("get_extended_info", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script), "GetExtendedInfo Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, arr,
                                                     error))
     return {};
@@ -146,6 +195,12 @@ ScriptedThreadPythonInterface::GetScriptedFramePluginName() {
   Status error;
   StructuredData::ObjectSP obj = Dispatch("get_scripted_frame_plugin", error);
 
+  if (error.Fail()) {
+    LLDB_LOG(GetLog(LLDBLog::Script),
+             "GetScriptedFramePluginName Python exception: {0}",
+             error.AsCString());
+  }
+
   if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
                                                     error))
     return {};
diff --git a/lldb/source/Target/ScriptedThreadPlan.cpp b/lldb/source/Target/ScriptedThreadPlan.cpp
index 7b59231f14d21..ddaf8dacf800b 100644
--- a/lldb/source/Target/ScriptedThreadPlan.cpp
+++ b/lldb/source/Target/ScriptedThreadPlan.cpp
@@ -20,6 +20,7 @@
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
 #include "lldb/Utility/State.h"
+#include "llvm/Support/FormatVariadic.h"
 
 using namespace lldb;
 using namespace lldb_private;
@@ -83,6 +84,10 @@ void ScriptedThreadPlan::DidPush() {
         m_class_name, this->shared_from_this(), m_args_data);
     if (!obj_or_err) {
       m_error_str = llvm::toString(obj_or_err.takeError());
+      Debugger::ReportError(
+          llvm::formatv("Failed to create ScriptedThreadPlan: {0}", m_error_str)
+              .str(),
+          m_process.GetTarget().GetDebugger().GetID());
       SetPlanComplete(false);
     } else
       m_implementation_sp = *obj_or_err;
diff --git a/lldb/test/API/functionalities/scripted_extensions/Makefile b/lldb/test/API/functionalities/scripted_extensions/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py b/lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py
new file mode 100644
index 0000000000000..5ffe7c316546f
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py
@@ -0,0 +1,124 @@
+"""
+Verify that exceptions raised inside scripted extension affordance methods
+(or missing required abstract methods) are surfaced to the user.
+
+For entry points with no return-channel for errors
+(`ScriptedProcess::CreateInstance`, `OperatingSystemPython` ctor,
+`ScriptedThreadPlan::DidPush`, `BreakpointResolverScripted` ctor) the
+diagnostic is broadcast via `Debugger::ReportError` and asserted on a
+listener.
+
+Entry points that already return an `llvm::Expected` / `Status`
+(`ScriptedThread::Create`, `ScriptedFrameProvider::CreateInstance`,
+`StopHookScripted::SetScriptCallback`) propagate the detailed error
+through their return type; tests for those are tracked as follow-up.
+"""
+
+import os
+
+import lldb
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.decorators import expectedFailureAll
+from lldbsuite.test.lldbtest import TestBase
+
+
+class TestScriptedExtensionsDiagnostics(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def setUp(self):
+        TestBase.setUp(self)
+        self.broadcaster = self.dbg.GetBroadcaster()
+        self.listener = lldbutil.start_listening_from(
+            self.broadcaster,
+            lldb.SBDebugger.eBroadcastBitWarning | lldb.SBDebugger.eBroadcastBitError,
+        )
+        script_path = os.path.join(
+            self.getSourceDir(), "malformed_scripted_extensions.py"
+        )
+        self.runCmd("command script import " + script_path)
+
+    def assert_diagnostic(self, expected_substring):
+        event = lldbutil.fetch_next_event(self, self.listener, self.broadcaster)
+        data = lldb.SBDebugger.GetDiagnosticFromEvent(event)
+        self.assertTrue(data.IsValid(), "event has diagnostic data")
+        message = data.GetValueForKey("message").GetStringValue(4096)
+        self.assertIn(expected_substring, message)
+
+    def create_target(self):
+        self.build()
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target, "valid target")
+        return target
+
+    # ------------------------------------------------------------------
+    # ScriptedProcess - reports via ScriptedProcess::CreateInstance
+    # ------------------------------------------------------------------
+
+    def test_scripted_process_missing_methods(self):
+        """A ScriptedProcess missing abstract methods should emit a
+        diagnostic naming the missing method."""
+        target = self.create_target()
+        launch_info = lldb.SBLaunchInfo(None)
+        launch_info.SetProcessPluginName("ScriptedProcess")
+        launch_info.SetScriptedProcessClassName(
+            "malformed_scripted_extensions.MissingMethodsScriptedProcess"
+        )
+        error = lldb.SBError()
+        target.Launch(launch_info, error)
+        self.assertTrue(error.Fail(), "launch should fail")
+        self.assert_diagnostic("read_memory_at_address")
+
+    # ------------------------------------------------------------------
+    # BreakpointResolverScripted - reports via
+    # BreakpointResolverScripted::CreateImplementationIfNeeded.
+    # `m_error` is set but never surfaced to the user, so ReportError is
+    # the only user-visible channel.
+    # ------------------------------------------------------------------
+
+    # TODO: malformed_scripted_extensions needs a class whose __init__
+    # raises (the current ExceptionScriptedBreakpointResolver only raises
+    # from __callback__, which goes through Dispatch/Status). Tracked as
+    # follow-up.
+    @expectedFailureAll(bugnumber="needs an init-raising malformed class")
+    def test_scripted_breakpoint_resolver_init_failure(self):
+        target = self.create_target()
+        target.BreakpointCreateFromScript(
+            "malformed_scripted_extensions.ExceptionScriptedBreakpointResolver",
+            lldb.SBStructuredData(),
+            lldb.SBFileSpecList(),
+            lldb.SBFileSpecList(),
+        )
+        self.assert_diagnostic("ExceptionScriptedBreakpointResolver")
+
+    # ------------------------------------------------------------------
+    # ScriptedThreadPlan - reports via ScriptedThreadPlan::DidPush. The
+    # plan stores the error in m_error_str but never surfaces it; the
+    # diagnostic is the user-visible channel.
+    # ------------------------------------------------------------------
+
+    @expectedFailureAll(bugnumber="needs an init-raising malformed class")
+    def test_scripted_thread_plan_init_failure(self):
+        target = self.create_target()
+        process = target.LaunchSimple(None, None, self.get_process_working_directory())
+        self.assertTrue(process, "valid process")
+        thread = process.GetSelectedThread()
+        self.assertTrue(thread, "valid thread")
+        thread.StepUsingScriptedThreadPlan(
+            "malformed_scripted_extensions.ExceptionScriptedThreadPlan"
+        )
+        self.assert_diagnostic("ExceptionScriptedThreadPlan")
+
+    # ------------------------------------------------------------------
+    # The remaining entry points need a live inferior process to trigger
+    # their creation paths, or have no plugin implementation yet.
+    # ------------------------------------------------------------------
+
+    @expectedFailureAll(
+        bugnumber="OperatingSystemPython needs a live process to load the OS plugin"
+    )
+    def test_operating_system_missing_methods(self):
+        self.assert_diagnostic("get_thread_info")
+
+    @expectedFailureAll(bugnumber="ScriptedPlatform has no plugin implementation yet")
+    def test_scripted_platform_missing_methods(self):
+        self.assert_diagnostic("list_processes")
diff --git a/lldb/test/API/functionalities/scripted_extensions/main.c b/lldb/test/API/functionalities/scripted_extensions/main.c
new file mode 100644
index 0000000000000..237c8ce181774
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/main.c
@@ -0,0 +1 @@
+int main() {}
diff --git a/lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py b/lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py
new file mode 100644
index 0000000000000..e9ff0fd17a57c
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py
@@ -0,0 +1,195 @@
+"""
+Intentionally malformed scripted extensions used by
+TestScriptedExtensionsDiagnostics.
+
+Each class either omits a required abstract method or raises a Python
+exception from one of its affordance methods. The corresponding test asserts
+that LLDB surfaces those errors to the user instead of silently swallowing
+them.
+"""
+
+# ---------------------------------------------------------------------------
+# Scripted Process
+# ---------------------------------------------------------------------------
+
+
+class MissingMethodsScriptedProcess:
+    """Missing required abstract method `read_memory_at_address`."""
+
+    def __init__(self, exe_ctx, args):
+        self.exe_ctx = exe_ctx
+        self.args = args
+
+    def get_scripted_thread_plugin(self):
+        return None
+
+    def is_alive(self):
+        return True
+
+
+class ExceptionScriptedProcess:
+    """All abstract methods present, but `launch` raises."""
+
+    def __init__(self, exe_ctx, args):
+        self.exe_ctx = exe_ctx
+        self.args = args
+
+    def get_scripted_thread_plugin(self):
+        return None
+
+    def is_alive(self):
+        return True
+
+    def read_memory_at_address(self, addr, size, error):
+        return None
+
+    def launch(self):
+        raise RuntimeError("intentional exception from launch()")
+
+
+# ---------------------------------------------------------------------------
+# Scripted Thread
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedThread:
+    def __init__(self, process, args):
+        self.process = process
+        self.args = args
+
+    def get_thread_id(self):
+        raise ValueError("intentional exception from get_thread_id()")
+
+    def get_register_context(self):
+        return ""
+
+    def get_name(self):
+        return "ExceptionScriptedThread"
+
+    def get_state(self):
+        return 0
+
+
+# ---------------------------------------------------------------------------
+# Scripted Platform
+# ---------------------------------------------------------------------------
+
+
+class MissingMethodsScriptedPlatform:
+    """Missing required abstract method `list_processes`."""
+
+    def __init__(self, exe_ctx, args):
+        self.exe_ctx = exe_ctx
+        self.args = args
+
+
+class ExceptionScriptedPlatform:
+    def __init__(self, exe_ctx, args):
+        self.exe_ctx = exe_ctx
+        self.args = args
+
+    def list_processes(self):
+        raise RuntimeError("intentional exception from list_processes()")
+
+    def get_process_info(self, pid):
+        return None
+
+    def launch_process(self, launch_info):
+        return None
+
+    def kill_process(self, pid):
+        return None
+
+
+# ---------------------------------------------------------------------------
+# Scripted Frame Provider
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedFrameProvider:
+    def __init__(self, frames, args):
+        self.frames = frames
+        self.args = args
+
+    def get_num_frames(self):
+        raise RuntimeError("intentional exception from get_num_frames()")
+
+    def get_frame_at_index(self, idx):
+        return None
+
+
+# ---------------------------------------------------------------------------
+# Scripted Thread Plan
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedThreadPlan:
+    def __init__(self, thread_plan, args):
+        self.thread_plan = thread_plan
+        self.args = args
+
+    def explains_stop(self, event):
+        raise RuntimeError("intentional exception from explains_stop()")
+
+    def should_stop(self, event):
+        return True
+
+    def is_stale(self):
+        return False
+
+
+# ---------------------------------------------------------------------------
+# Scripted Breakpoint Resolver
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedBreakpointResolver:
+    def __init__(self, bkpt, args):
+        self.bkpt = bkpt
+        self.args = args
+
+    def __callback__(self, sym_ctx):
+        raise RuntimeError("intentional exception from __callback__()")
+
+    def get_short_help(self):
+        return "Exception breakpoint resolver"
+
+
+# ---------------------------------------------------------------------------
+# Scripted Stop Hook
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedStopHook:
+    def __init__(self, target, args):
+        self.target = target
+        self.args = args
+
+    def handle_stop(self, exe_ctx, stream):
+        raise RuntimeError("intentional exception from handle_stop()")
+
+
+# ---------------------------------------------------------------------------
+# Operating System
+# ---------------------------------------------------------------------------
+
+
+class MissingMethodsOperatingSystem:
+    """Missing required abstract method `get_thread_info`."""
+
+    def __init__(self, process):
+        self.process = process
+
+
+class ExceptionOperatingSystem:
+    def __init__(self, process):
+        self.process = process
+
+    def get_thread_info(self):
+        raise RuntimeError("intentional exception from get_thread_info()")
+
+    def get_register_info(self):
+        return {}
+
+    def get_register_data(self, tid):
+        return b""



More information about the lldb-commits mailing list