diff --git a/src/python.ts b/src/python.ts index 0b39e4a..8054590 100644 --- a/src/python.ts +++ b/src/python.ts @@ -154,13 +154,38 @@ export class Callback { args: Deno.PointerValue, kwargs: Deno.PointerValue, ) => { - return PyObject.from(callback( - kwargs === null ? {} : Object.fromEntries( - new PyObject(kwargs).asDict() - .entries(), - ), - ...(args === null ? [] : new PyObject(args).valueOf()), - )).handle; + let result: PythonConvertible; + // Prepare arguments for the JS callback + try { + // Prepare arguments for the JS callback + const jsKwargs = kwargs === null + ? {} + : Object.fromEntries(new PyObject(kwargs).asDict().entries()); + const jsArgs = args === null ? [] : new PyObject(args).valueOf(); + + // Call the actual JS function + result = callback(jsKwargs, ...jsArgs); + + // Convert the JS return value back to a Python object + return PyObject.from(result).handle; + } catch (e) { + // An error occurred in the JS callback. + // We need to set a Python exception and return NULL. + + // Prepare the error message for Python + const errorMessage = e instanceof Error + ? `${e.name}: ${e.message}` // Include JS error type and message + : String(e); // Fallback for non-Error throws + const cErrorMessage = cstr(`JS Callback Error: ${errorMessage}`); + + const errorTypeHandle = + python.builtins.RuntimeError[ProxiedPyObject].handle; + + // Set the Python exception (type and message) + py.PyErr_SetString(errorTypeHandle, cErrorMessage); + + return null; + } }, ); } diff --git a/src/symbols.ts b/src/symbols.ts index 1bd0def..c7a0abc 100644 --- a/src/symbols.ts +++ b/src/symbols.ts @@ -39,6 +39,11 @@ export const SYMBOLS = { result: "void", }, + PyErr_SetString: { + parameters: ["pointer", "buffer"], // type, message + result: "void", + }, + PyDict_New: { parameters: [], result: "pointer", diff --git a/test/test.ts b/test/test.ts index 796a159..8293618 100644 --- a/test/test.ts +++ b/test/test.ts @@ -360,3 +360,27 @@ Deno.test("callbacks have signature", async (t) => { fn.destroy(); }); }); + +Deno.test("js exception inside python callback returns python exception", () => { + const pyCallback = python.callback(() => { + throw new Error("This is an intentional error from JS!"); + }); + + const pyModule = python.runModule( + ` +def call_the_callback(cb): + result = cb() + return result + `, + "test_module", + ); + + try { + pyModule.call_the_callback(pyCallback); + } catch (e) { + // deno-lint-ignore no-explicit-any + assertEquals((e as any).name, "PythonError"); + } finally { + pyCallback.destroy(); + } +});