Skip to content

Commit 6501685

Browse files
committed
[GR-62079] Backport to 24.2: Error message implies Promise rejected in sync modules.
PullRequest: js/3414
2 parents 5f3037e + 3d045d5 commit 6501685

File tree

8 files changed

+236
-88
lines changed

8 files changed

+236
-88
lines changed

graal-js/src/com.oracle.truffle.js.parser/src/com/oracle/truffle/js/parser/GraalJSEvaluator.java

Lines changed: 32 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -304,19 +304,19 @@ private Object evalModule(JSRealm realm) {
304304
// Note: If loading failed, we must not perform module linking.
305305

306306
moduleRecord.link(realm);
307-
Object promise = moduleRecord.evaluate(realm);
308-
boolean isAsync = context.isOptionTopLevelAwait() && moduleRecord.isAsyncEvaluation();
309-
if (isAsync) {
310-
JSFunctionObject onRejected = createTopLevelAwaitReject(context, realm);
311-
JSFunctionObject onAccepted = createTopLevelAwaitResolve(context, realm);
312-
// Non-standard: throw error from onRejected handler.
313-
performPromiseThenNode.execute((JSPromiseObject) promise, onAccepted, onRejected, null);
314-
}
307+
// On failure, an exception is thrown and this module's [[Status]] remains unlinked.
308+
309+
JSPromiseObject promise = moduleRecord.evaluate(realm);
310+
JSFunctionObject onRejected = createTopLevelAwaitReject(context, realm);
311+
JSFunctionObject onAccepted = createTopLevelAwaitResolve(context, realm);
312+
// Non-standard: throw error from onRejected handler.
313+
performPromiseThenNode.execute(promise, onAccepted, onRejected, null);
314+
315315
if (context.getLanguageOptions().esmEvalReturnsExports()) {
316316
JSDynamicObject moduleNamespace = moduleRecord.getModuleNamespace();
317317
assert moduleNamespace != null;
318318
return moduleNamespace;
319-
} else if (isAsync) {
319+
} else if (context.isOptionTopLevelAwait() && moduleRecord.isAsyncEvaluation()) {
320320
return promise;
321321
} else {
322322
return moduleRecord.getExecutionResultOrThrow();
@@ -700,47 +700,32 @@ private int innerModuleLinking(JSRealm realm, AbstractModuleRecord abstractModul
700700

701701
@TruffleBoundary
702702
@Override
703-
public Object moduleEvaluation(JSRealm realm, CyclicModuleRecord moduleRecord) {
703+
public JSPromiseObject moduleEvaluation(JSRealm realm, CyclicModuleRecord moduleRecord) {
704704
// Evaluate ( ) Concrete Method
705705
CyclicModuleRecord module = moduleRecord;
706+
assert module.getStatus() == Status.Linked || module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated : module.getStatus();
707+
if (module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated) {
708+
module = module.getCycleRoot();
709+
}
710+
if (module.getTopLevelCapability() != null) {
711+
return (JSPromiseObject) module.getTopLevelCapability().getPromise();
712+
}
706713
Deque<CyclicModuleRecord> stack = new ArrayDeque<>(4);
707-
if (realm.getContext().isOptionTopLevelAwait()) {
708-
assert module.getStatus() == Status.Linked || module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated : module.getStatus();
709-
if (module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated) {
710-
module = module.getCycleRoot();
711-
}
712-
if (module.getTopLevelCapability() != null) {
713-
return module.getTopLevelCapability().getPromise();
714-
}
715-
PromiseCapabilityRecord capability = NewPromiseCapabilityNode.createDefault(realm);
716-
module.setTopLevelCapability(capability);
717-
try {
718-
innerModuleEvaluation(realm, module, stack, 0);
719-
assert module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated;
720-
assert module.getEvaluationError() == null;
721-
if (!module.isAsyncEvaluation()) {
722-
assert module.getStatus() == Status.Evaluated;
723-
JSFunction.call(JSArguments.create(Undefined.instance, capability.getResolve(), Undefined.instance));
724-
}
725-
assert stack.isEmpty();
726-
} catch (AbstractTruffleException e) {
727-
handleModuleEvaluationError(module, stack, e);
728-
}
729-
return capability.getPromise();
730-
} else {
731-
try {
732-
innerModuleEvaluation(realm, module, stack, 0);
733-
} catch (AbstractTruffleException e) {
734-
handleModuleEvaluationError(module, stack, e);
735-
throw e;
736-
}
714+
PromiseCapabilityRecord capability = NewPromiseCapabilityNode.createDefault(realm);
715+
module.setTopLevelCapability(capability);
716+
try {
717+
innerModuleEvaluation(realm, module, stack, 0);
737718
assert module.getStatus() == Status.EvaluatingAsync || module.getStatus() == Status.Evaluated;
738719
assert module.getEvaluationError() == null;
739-
720+
if (!module.isAsyncEvaluation()) {
721+
assert module.getStatus() == Status.Evaluated;
722+
JSFunction.call(JSArguments.create(Undefined.instance, capability.getResolve(), Undefined.instance));
723+
}
740724
assert stack.isEmpty();
741-
Object result = module.getExecutionResult();
742-
return result == null ? Undefined.instance : result;
725+
} catch (AbstractTruffleException e) {
726+
handleModuleEvaluationError(module, stack, e);
743727
}
728+
return (JSPromiseObject) capability.getPromise();
744729
}
745730

746731
private static void handleModuleEvaluationError(CyclicModuleRecord module, Deque<CyclicModuleRecord> stack, AbstractTruffleException e) {
@@ -752,9 +737,7 @@ private static void handleModuleEvaluationError(CyclicModuleRecord module, Deque
752737
assert module.getStatus() == Status.Evaluated && module.getEvaluationError() == e;
753738

754739
PromiseCapabilityRecord capability = module.getTopLevelCapability();
755-
if (capability != null) {
756-
JSFunction.call(JSArguments.create(Undefined.instance, capability.getReject(), getErrorObject(e)));
757-
}
740+
JSFunction.call(JSArguments.create(Undefined.instance, capability.getReject(), getErrorObject(e)));
758741
}
759742

760743
private static Object getErrorObject(AbstractTruffleException e) {
@@ -769,13 +752,9 @@ private int innerModuleEvaluation(JSRealm realm, AbstractModuleRecord abstractMo
769752
// InnerModuleEvaluation( module, stack, index )
770753
int index = index0;
771754
if (!(abstractModule instanceof CyclicModuleRecord moduleRecord)) {
772-
Object result = abstractModule.evaluate(realm);
773-
if (result instanceof JSPromiseObject promise) {
774-
assert !JSPromise.isPending(promise);
775-
if (JSPromise.isRejected(promise)) {
776-
throw JSRuntime.getException(JSPromise.getPromiseResult(promise));
777-
}
778-
}
755+
JSPromiseObject promise = abstractModule.evaluate(realm);
756+
assert !JSPromise.isPending(promise);
757+
JSPromise.throwIfRejected(promise, realm);
779758
return index;
780759
}
781760
if (moduleRecord.getStatus() == Status.EvaluatingAsync || moduleRecord.getStatus() == Status.Evaluated) {
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.truffle.js.test.regress;
42+
43+
import static com.oracle.truffle.js.runtime.JSContextOptions.UNHANDLED_REJECTIONS_NAME;
44+
import static org.hamcrest.CoreMatchers.containsString;
45+
import static org.hamcrest.CoreMatchers.equalTo;
46+
import static org.hamcrest.CoreMatchers.not;
47+
import static org.hamcrest.MatcherAssert.assertThat;
48+
import static org.junit.Assert.assertFalse;
49+
import static org.junit.Assert.assertTrue;
50+
import static org.junit.Assert.fail;
51+
52+
import java.io.ByteArrayOutputStream;
53+
import java.io.IOException;
54+
import java.util.List;
55+
import java.util.Map;
56+
57+
import org.graalvm.polyglot.Context;
58+
import org.graalvm.polyglot.PolyglotException;
59+
import org.graalvm.polyglot.Source;
60+
import org.graalvm.polyglot.io.IOAccess;
61+
import org.junit.Test;
62+
import org.junit.runner.RunWith;
63+
import org.junit.runners.Parameterized;
64+
import org.junit.runners.Parameterized.Parameter;
65+
import org.junit.runners.Parameterized.Parameters;
66+
67+
import com.oracle.truffle.js.test.JSTest;
68+
import com.oracle.truffle.js.test.polyglot.MockFileSystem;
69+
70+
@RunWith(Parameterized.class)
71+
public class GR61889 {
72+
73+
@Parameters(name = "unhandled-rejections={0}")
74+
public static Iterable<String> data() {
75+
return List.of("none", "throw", "warn");
76+
}
77+
78+
@Parameter(0) public String unhandledRejections;
79+
80+
/**
81+
* Do not treat rejected module evaluation promises as unhandled rejections.
82+
*/
83+
@Test
84+
public void testRejectedModulePromiseIsHandled() throws IOException {
85+
var fs = new MockFileSystem(Map.of(
86+
"eval-throws-error.mjs", """
87+
export function foo() {return 42}
88+
throw new Error("Thrown from within the library");
89+
""",
90+
"eval-throws-non-error.mjs", """
91+
export function foo() {return 42}
92+
throw "Thrown from within the library";
93+
"""));
94+
95+
try (var out = new ByteArrayOutputStream();
96+
Context c = JSTest.newContextBuilder().//
97+
option(UNHANDLED_REJECTIONS_NAME, unhandledRejections).//
98+
allowIO(IOAccess.newBuilder().fileSystem(fs).build()).//
99+
out(out).err(out).build()) {
100+
// Error thrown from another synchronously executed module.
101+
for (String moduleCode : List.of(
102+
"import * as throws from './eval-throws-error.mjs';",
103+
"import * as throws from './eval-throws-non-error.mjs';")) {
104+
try {
105+
c.eval(Source.newBuilder("js", moduleCode, "main.mjs").buildLiteral());
106+
fail("should have thrown");
107+
} catch (PolyglotException e) {
108+
assertFalse(e.getMessage(), e.isSyntaxError());
109+
assertTrue(e.getMessage(), e.isGuestException());
110+
assertThat(e.getMessage(), containsString("Thrown from within the library"));
111+
assertThat(e.getMessage(), not(containsString("Unhandled promise rejection")));
112+
}
113+
}
114+
assertThat("No unhandled rejection warnings", out.toString(), equalTo(""));
115+
}
116+
117+
try (var out = new ByteArrayOutputStream();
118+
Context c = JSTest.newContextBuilder().//
119+
option(UNHANDLED_REJECTIONS_NAME, unhandledRejections).//
120+
allowIO(IOAccess.newBuilder().fileSystem(fs).build()).//
121+
out(out).err(out).build()) {
122+
// Error object thrown from the main module.
123+
for (String moduleCode : List.of("""
124+
export function foo() {return 42}
125+
throw new Error("Thrown from within main module");
126+
""", """
127+
export function foo() {return 42}
128+
throw "Thrown from within main module";
129+
""")) {
130+
try {
131+
c.eval(Source.newBuilder("js", moduleCode, "main.mjs").buildLiteral());
132+
fail("should have thrown");
133+
} catch (PolyglotException e) {
134+
assertFalse(e.getMessage(), e.isSyntaxError());
135+
assertTrue(e.getMessage(), e.isGuestException());
136+
assertThat(e.getMessage(), containsString("Thrown from within main module"));
137+
assertThat(e.getMessage(), not(containsString("Unhandled promise rejection")));
138+
}
139+
assertThat("No unhandled rejection warnings", out.toString(), equalTo(""));
140+
}
141+
}
142+
}
143+
144+
/**
145+
* Do not treat rejected module loading promises as unhandled rejections.
146+
*/
147+
@Test
148+
public void testRejectedLoadRequestedModulesPromiseIsHandled() throws IOException {
149+
var fs = new MockFileSystem(Map.of());
150+
try (var out = new ByteArrayOutputStream();
151+
Context c = JSTest.newContextBuilder().//
152+
option(UNHANDLED_REJECTIONS_NAME, unhandledRejections).//
153+
allowIO(IOAccess.newBuilder().fileSystem(fs).build()).//
154+
out(out).err(out).build()) {
155+
try {
156+
c.eval(Source.newBuilder("js", """
157+
import * as throws from "./does_not_exist.mjs";
158+
""", "./throws-during-load.mjs").buildLiteral());
159+
fail("should have thrown");
160+
} catch (PolyglotException e) {
161+
assertFalse(e.getMessage(), e.isSyntaxError());
162+
assertTrue(e.getMessage(), e.isGuestException());
163+
assertThat(e.getMessage(), containsString("Cannot find module"));
164+
assertThat(e.getMessage(), not(containsString("Unhandled promise rejection")));
165+
}
166+
assertThat(out.toString(), equalTo(""));
167+
}
168+
}
169+
}

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/promise/ImportCallNode.java

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -81,7 +81,6 @@
8181
import com.oracle.truffle.js.runtime.builtins.JSPromise;
8282
import com.oracle.truffle.js.runtime.builtins.JSPromiseObject;
8383
import com.oracle.truffle.js.runtime.objects.AbstractModuleRecord;
84-
import com.oracle.truffle.js.runtime.objects.CyclicModuleRecord;
8584
import com.oracle.truffle.js.runtime.objects.JSDynamicObject;
8685
import com.oracle.truffle.js.runtime.objects.JSObject;
8786
import com.oracle.truffle.js.runtime.objects.PromiseCapabilityRecord;
@@ -381,19 +380,9 @@ protected Object executeInRealm(VirtualFrame frame) {
381380
// If link is an abrupt completion, reject the promise from import().
382381
moduleRecord.link(realm);
383382

384-
// Evaluate() should always return a promise.
385-
// Yet, if top-level-await is disabled, returns/throws the result instead.
386-
Object evaluatePromise = moduleRecord.evaluate(realm);
387-
if (context.isOptionTopLevelAwait() || !(moduleRecord instanceof CyclicModuleRecord cyclicModuleRecord)) {
388-
assert evaluatePromise instanceof JSPromiseObject : evaluatePromise;
389-
JSFunctionObject onFulfilled = createFulfilledClosure(context, realm, captures);
390-
promiseThenNode.execute((JSPromiseObject) evaluatePromise, onFulfilled, onRejected);
391-
} else {
392-
// Rethrow any previous execution errors.
393-
cyclicModuleRecord.getExecutionResultOrThrow();
394-
var namespace = moduleRecord.getModuleNamespace();
395-
callPromiseResolve.executeCall(JSArguments.createOneArg(Undefined.instance, importPromiseCapability.getResolve(), namespace));
396-
}
383+
JSPromiseObject evaluatePromise = moduleRecord.evaluate(realm);
384+
JSFunctionObject onFulfilled = createFulfilledClosure(context, realm, captures);
385+
promiseThenNode.execute(evaluatePromise, onFulfilled, onRejected);
397386
} catch (AbstractTruffleException ex) {
398387
rejectPromise(importPromiseCapability, ex);
399388
}

0 commit comments

Comments
 (0)