@@ -218,3 +218,74 @@ Alternately, to ignore the error, call `PyErr_Clear()
218218<https://docs.python.org/3/c-api/exceptions.html#c.PyErr_Clear> `__. Any
219219Python error must be thrown or cleared, or nanobind will be left in an
220220invalid state.
221+
222+ .. _exception_chaining :
223+
224+ Chaining exceptions ('raise from')
225+ ----------------------------------
226+
227+ Python has a mechanism for indicating that exceptions were caused by other
228+ exceptions:
229+
230+ .. code-block :: py
231+
232+ try :
233+ print (1 / 0 )
234+ except Exception as exc:
235+ raise RuntimeError (" could not divide by zero" ) from exc
236+
237+ To do a similar thing in pybind11, you can use the :cpp:func: `nb::raise_from
238+ <raise_from> ` function, which requires a :cpp:class: `nb::python_error
239+ <python_error> ` and re-raises it with a chained exception object.
240+
241+ .. code-block :: cpp
242+
243+ nb::callable f = ...;
244+ int arg = 123;
245+ try {
246+ f(arg);
247+ } catch (nb::python_error &e) {
248+ nb::raise_from(e, PyExc_RuntimeError, "Could not call 'f' with %i", arg);
249+ }
250+
251+ The function is internally based on the Python function ``PyErr_FormatV `` and
252+ takes ``printf ``-style arguments following the format descriptor.
253+
254+ An even lower-level interface is available via :cpp:func: `nb::chain_error
255+ <chain_error> `.
256+
257+ Handling unraisable exceptions
258+ ------------------------------
259+
260+ If a Python function invoked from a C++ destructor or any function marked
261+ ``noexcept(true) `` (collectively, "noexcept functions") throws an exception, there
262+ is no way to propagate the exception, as such functions may not throw.
263+ Should they throw or fail to catch any exceptions in their call graph,
264+ the C++ runtime calls ``std::terminate() `` to abort immediately.
265+
266+ Similarly, Python exceptions raised in a class's ``__del__ `` method do not
267+ propagate, but are logged by Python as an unraisable error. In Python 3.8+, a
268+ `system hook is triggered
269+ <https://docs.python.org/3/library/sys.html#sys.unraisablehook> `_
270+ and an auditing event is logged.
271+
272+ Any noexcept function should have a try-catch block that traps
273+ :cpp:class: `nb::python_error <python_error> ` (or any other exception that can
274+ occur). A useful approach is to convert them to Python exceptions and then
275+ ``discard_as_unraisable `` as shown below.
276+
277+ .. code-block :: cpp
278+
279+ void nonthrowing_func() noexcept(true) {
280+ try {
281+ // ...
282+ } catch (nb::python_error &e) {
283+ // Discard the Python error using Python APIs, using the C++ magic
284+ // variable __func__. Python already knows the type and value and of the
285+ // exception object.
286+ e.discard_as_unraisable(__func__);
287+ } catch (const std::exception &e) {
288+ // Log and discard C++ exceptions.
289+ third_party::log(e);
290+ }
291+ }
0 commit comments