Skip to content

Commit 05795f1

Browse files
Merge pull request #365 from Distributive-Network/Xmader/fix/Future-exception-never-retrieved
Fix the `Future exception not retrieved` issue
2 parents 6b75d8f + 665af9f commit 05795f1

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

python/pythonmonkey/helpers.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,19 @@ def new(ctor):
4343
return (lambda *args: newCtor(list(args)))
4444

4545

46+
def simpleUncaughtExceptionHandler(loop, context):
47+
"""
48+
A simple exception handler for uncaught JS Promise rejections sent to the Python event-loop
49+
50+
See https://docs.python.org/3.11/library/asyncio-eventloop.html#error-handling-api
51+
"""
52+
error = context["exception"]
53+
pm.eval("(err) => console.error('Uncaught', err)")(error)
54+
pm.stop() # unblock `await pm.wait()` to gracefully exit the program
55+
56+
4657
# List which symbols are exposed to the pythonmonkey module.
47-
__all__ = ["new", "typeof"]
58+
__all__ = ["new", "typeof", "simpleUncaughtExceptionHandler"]
4859

4960
# Add the non-enumerable properties of globalThis which don't collide with pythonmonkey.so as exports:
5061
globalThis = pm.eval('globalThis')

src/JobQueue.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010

1111
#include "include/JobQueue.hh"
12+
#include "include/modules/pythonmonkey/pythonmonkey.hh"
1213

1314
#include "include/PyEventLoop.hh"
1415
#include "include/pyTypeFactory.hh"
@@ -134,6 +135,21 @@ void JobQueue::promiseRejectionTracker(JSContext *cx,
134135
return;
135136
}
136137

138+
// Test if there's no user-defined (or pmjs defined) exception handler on the Python event-loop
139+
PyEventLoop loop = PyEventLoop::getRunningLoop();
140+
if (!loop.initialized()) return;
141+
PyObject *customHandler = PyObject_GetAttrString(loop._loop, "_exception_handler"); // see https://github.com/python/cpython/blob/v3.9.16/Lib/asyncio/base_events.py#L1782
142+
if (customHandler == Py_None) { // we only have the default exception handler
143+
// Set an exception handler to the event-loop
144+
PyObject *pmModule = PyImport_ImportModule("pythonmonkey");
145+
PyObject *exceptionHandler = PyObject_GetAttrString(pmModule, "simpleUncaughtExceptionHandler");
146+
PyObject_CallMethod(loop._loop, "set_exception_handler", "O", exceptionHandler);
147+
Py_DECREF(pmModule);
148+
Py_DECREF(exceptionHandler);
149+
}
150+
Py_DECREF(customHandler);
151+
152+
// Go ahead and send this unhandled Promise rejection to the exception handler on the Python event-loop
137153
PyObject *pyFuture = PromiseType::getPyObject(cx, promise); // ref count == 2
138154
// Unhandled Future object calls the event-loop exception handler in its destructor (the `__del__` magic method)
139155
// See https://github.com/python/cpython/blob/v3.9.16/Lib/asyncio/futures.py#L108
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#! /bin/bash
2+
#
3+
# @file uncaught-rejection-handler.bash
4+
# For testing if the actual JS error gets printed out for uncaught Promise rejections,
5+
# instead of printing out a Python `Future exception was never retrieved` error message when not in pmjs
6+
#
7+
# @author Tom Tang ([email protected])
8+
# @date June 2024
9+
10+
set -u
11+
set -o pipefail
12+
13+
panic()
14+
{
15+
echo "FAIL: $*" >&2
16+
exit 2
17+
}
18+
19+
cd `dirname "$0"` || panic "could not change to test directory"
20+
21+
code='
22+
import asyncio
23+
import pythonmonkey as pm
24+
25+
async def pythonmonkey_main():
26+
pm.eval("""void Promise.reject(new TypeError("abc"));""")
27+
await pm.wait()
28+
29+
asyncio.run(pythonmonkey_main())
30+
'
31+
32+
OUTPUT=$(python -c "$code" \
33+
< /dev/null 2>&1
34+
)
35+
36+
echo "$OUTPUT" \
37+
| tr -d '\r' \
38+
| (grep -c 'Future exception was never retrieved' || true) \
39+
| while read qty
40+
do
41+
echo "$OUTPUT"
42+
[ "$qty" != "0" ] && panic "There shouldn't be a 'Future exception was never retrieved' error massage"
43+
break
44+
done || exit $?
45+
46+
echo "$OUTPUT" \
47+
| tr -d '\r' \
48+
| grep -c 'Uncaught TypeError: abc' \
49+
| while read qty
50+
do
51+
[ "$qty" != "1" ] && panic "It should print out 'Uncaught TypeError' directly"
52+
break
53+
done || exit $?

0 commit comments

Comments
 (0)