Repository: QwikDev/qwik
Pull request #7470: fix: retry computed qrl execution when not ready
Author: login: JerryWu1234
Labels: name: V2
Created: 2025-04-01T03:08:12Z | Merged: 2025-04-08T05:45:03Z

<!--
The Qwik Team and Community appreciate all PRs. Thank you for your effort! Not all PRs can be merged, but those that meet the following criteria will be prioritized:

a) Core fixes, and

b) Framework functionality achievable only by the core.

If this PR can be done as a 3rd-Party Community Add-On, we encourage that for quicker adoption.

If you believe your functionality is valuable to the entire Qwik Community, discuss it in the Qwik Discord channels for potential inclusion in the core.

First of all, make sure your PR title is descriptive and matches our commit title guidelines.

Also make sure your PR follows all the guidelines in the [CONTRIBUTING.md](./CONTRIBUTING.md) document.

-->

# What is it?

fixes #7442

feat(qrl): add e2e test and retry logic for QRL resolution

Add a new e2e test to verify QRL behavior without uncaught promises. Introduce retry logic for QRL resolution to handle potential promise retries, ensuring robustness in asynchronous operations.

<!-- pick one and remove the others -->

- Feature / enhancement
- Bug
- Docs / tests / types / typos
- Infra

# Description

<!--
* Include a summary of the motivation and context for this PR
* Is it related to any opened issues? (please add them here)
-->

# Checklist

<!--
* delete the items that are not relevant, so it's easy to tell if the PR is ready to be merged
* add items that are relevant and need to be done before merging
-->

- [x] My code follows the [developer guidelines of this project](https://github.com/QwikDev/qwik/blob/main/CONTRIBUTING.md)
- [x] I performed a self-review of my own code
- [x] I added a changeset with `pnpm change`
- [x] I made corresponding changes to the Qwik docs
- [x] I added new tests to cover the fix / functionality


Linked issue #7442: [🐞] [v2] thrown Promise isn't caught when not awaiting
### Which component is affected?

Qwik Runtime

### Describe the bug

When the dev calls a qrl without awaiting it, and it needs to be fetched, that will throw a Promise which will be uncaught.


### Reproduction

https://qwikdev-build-v2.qwik-8nx.pages.dev/playground/#f=7VbLCsJADPyVpaeFPtCDiGA96EnQg%2FYXLFgonvRU%2Bu9Okq3N6rYo4s1LoW2SzYTZySjSzGcPOJo0icdgfpNdnJg3bIrAL7B9atIqUrLtBUrRFTwcd5hLkGySeoVsI8K7RRIzAYKekl7VNVsUzs0beraYhJDUbyyUpdtZYkeurGWr89QYSVrBWxA82tBaguBwozIeK%2F0FghGlcZpgqQz6civjmGoY8VoZIZHv9JXRdAecsa%2Fqko3IeHnLXVGueaGhODtTnfKoosGkNAiaTCp%2Fot7rNOrAtiOZHL8vTTOMqHUUdC7SEZHb%2Bc7ZjnPMXQTtcIcla7pQpvwTyfJuwV%2BwfiFYdw

### Steps to reproduce

click the button

### System Info

```shell
web
```

### Additional Information

It's really a mistake by the dev but if we can fix it we should.

Likely we'll only be able to handle the sync case, by handling retries inside the qrl apply
Linked issue comments: user: login: JerryWu1234
created_at: 2025-03-21T02:19:27Z
body: @wmertens 
Are you doing right now ?
author_association: CONTRIBUTOR
user: login: wmertens
created_at: 2025-03-21T14:23:48Z
body: @JerryWu1234 no, feel free to take a look at `retryOnPromise` and make it so calling the qrl normally can retry

author_association: MEMBER
user: login: JerryWu1234
created_at: 2025-03-24T01:18:48Z
body: > [@JerryWu1234](https://github.com/JerryWu1234) no, feel free to take a look at `retryOnPromise` and make it so calling the qrl normally can retry

Sorry for replying late. 
now, have you already fixed it ?
author_association: CONTRIBUTOR
user: login: wmertens
created_at: 2025-03-24T05:13:10Z
body: Not fixed yet :)
author_association: MEMBER
user: login: JerryWu1234
created_at: 2025-03-24T08:48:42Z
body: thanks, I'll deep into it this week 
author_association: CONTRIBUTOR
user: login: JerryWu1234
created_at: 2025-04-08T08:36:53Z
body: @wmertens going to close this 
author_association: CONTRIBUTOR

Diff:
diff --git a/.changeset/sweet-hairs-remember.md b/.changeset/sweet-hairs-remember.md
new file mode 100644
index 00000000000..a74444f9d41
--- /dev/null
+++ b/.changeset/sweet-hairs-remember.md
@@ -0,0 +1,5 @@
+---
+'@qwik.dev/core': patch
+---
+
+FIX: Introduce retry logic for QRL resolution to handle potential promise retries, ensuring robustness in asynchronous operations.
diff --git a/packages/qwik/src/core/shared/qrl/qrl-class.ts b/packages/qwik/src/core/shared/qrl/qrl-class.ts
index d11fa9b56a2..edd2746ed97 100644
--- a/packages/qwik/src/core/shared/qrl/qrl-class.ts
+++ b/packages/qwik/src/core/shared/qrl/qrl-class.ts
@@ -12,7 +12,7 @@ import {
   type InvokeTuple,
 } from '../../use/use-core';
 import { getQFuncs, QInstanceAttr } from '../utils/markers';
-import { isPromise, maybeThen } from '../utils/promises';
+import { isPromise, maybeThen, retryOnPromise } from '../utils/promises';
 import { qDev, qSerialize, qTest, seal } from '../utils/qdev';
 import { isArray, isFunction, type ValueOrPromise } from '../utils/types';
 import type { QRLDev } from './qrl';
@@ -93,7 +93,8 @@ export const createQRL = <TYPE>(
     // Note that we bind the current `this`
     const bound = (...args: QrlArgs<TYPE>): ValueOrPromise<QrlReturn<TYPE> | undefined> => {
       if (!qrl.resolved) {
-        return qrl.resolve().then((fn) => {
+        const promise = retryOnPromise(() => qrl.resolve()) as Promise<TYPE>;
+        return promise.then((fn) => {
           if (!isFunction(fn)) {
             throw qError(QError.qrlIsNotFunction);
           }
diff --git a/starters/apps/e2e/src/components/qrl/qrl.tsx b/starters/apps/e2e/src/components/qrl/qrl.tsx
new file mode 100644
index 00000000000..ebc90e951c6
--- /dev/null
+++ b/starters/apps/e2e/src/components/qrl/qrl.tsx
@@ -0,0 +1,32 @@
+import { $, component$, useComputed$, useSignal } from "@qwik.dev/core";
+
+export const QRL = component$(() => {
+  return (
+    <>
+      <ShouldResolveInnerComputedQRL />
+    </>
+  );
+});
+
+export const ShouldResolveInnerComputedQRL = component$(() => {
+  const test = useComputed$(() => 0);
+  return <InnerComputedButton test={test} />;
+});
+
+export const InnerComputedButton = component$<any>((props) => {
+  const syncSelectionCounter = useSignal(0);
+  const syncSelection = $(() => {
+    syncSelectionCounter.value++;
+    props.test.value;
+  });
+
+  const handleClick = $(() => {
+    syncSelection();
+  });
+
+  return (
+    <button id="inner-computed-button" onClick$={handleClick}>
+      Click Me {syncSelectionCounter.value}
+    </button>
+  );
+});
diff --git a/starters/apps/e2e/src/root.tsx b/starters/apps/e2e/src/root.tsx
index fa8e56aa3c7..e82fd7f1186 100644
--- a/starters/apps/e2e/src/root.tsx
+++ b/starters/apps/e2e/src/root.tsx
@@ -35,6 +35,7 @@ import { UseId } from "./components/useid/useid";
 import { Watch } from "./components/watch/watch";
 
 import "./global.css";
+import { QRL } from "./components/qrl/qrl";
 
 const tests: Record<string, FunctionComponent> = {
   "/e2e/two-listeners": () => <TwoListeners />,
@@ -71,6 +72,7 @@ const tests: Record<string, FunctionComponent> = {
   "/e2e/build-variables": () => <BuildVariables />,
   "/e2e/exception/render": () => <RenderExceptions />,
   "/e2e/exception/use-task": () => <UseTaskExceptions />,
+  "/e2e/qrl": () => <QRL />,
 };
 
 export const Root = component$<{ pathname: string }>(({ pathname }) => {
diff --git a/starters/e2e/e2e.qrl.e2e.ts b/starters/e2e/e2e.qrl.e2e.ts
new file mode 100644
index 00000000000..34ad27aa114
--- /dev/null
+++ b/starters/e2e/e2e.qrl.e2e.ts
@@ -0,0 +1,14 @@
+import { expect, test } from "@playwright/test";
+test("should update counter without uncaught promises", async ({ page }) => {
+  await page.goto("/e2e/qrl");
+  page.on("pageerror", (err) => expect(err).toEqual(undefined));
+  page.on("console", (msg) => {
+    if (msg.type() === "error") {
+      expect(msg.text()).toEqual(undefined);
+    }
+  });
+  const button = page.locator("#inner-computed-button");
+  await expect(button).toContainText("Click Me 0");
+
+  await button.click();
+});


Reviews:
user: login: wmertens
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-03T09:09:13Z
user: login: Varixo
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-06T14:28:00Z
user: login: Varixo
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-07T19:10:22Z
user: login: wmertens
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T05:40:20Z
user: login: JerryWu1234
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T05:41:59Z
user: login: wmertens
body: Great @JerryWu1234 ! LGTM 🚀
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-08T05:44:25Z

Review comments:
diff_hunk: @@ -0,0 +1,15 @@
+import { expect, test } from "@playwright/test";
+test("should update counter without uncaught promises", async ({ page }) => {
+  await page.goto("/e2e/qrl");
+  page.on("pageerror", (err) => expect(err).toEqual(undefined));
+  page.on("console", (msg) => {
+    if (msg.type() === "error") {
+      expect(msg.text()).toEqual(undefined);
+    }
+  });
+  const button = page.locator("#inner-computed-button");
+  await expect(button).toContainText("Click Me 0");
+
+  // 先执行点击操作
path: starters/e2e/e2e.qrl.e2e.ts
user: login: wmertens
body: comments in English only please :)
Here I don't think a comment is needed actually
created_at: 2025-04-03T09:07:05Z
author_association: MEMBER
diff_hunk: @@ -93,12 +93,14 @@ export const createQRL = <TYPE>(
     // Note that we bind the current `this`
     const bound = (...args: QrlArgs<TYPE>): ValueOrPromise<QrlReturn<TYPE> | undefined> => {
       if (!qrl.resolved) {
-        return qrl.resolve().then((fn) => {
-          if (!isFunction(fn)) {
-            throw qError(QError.qrlIsNotFunction);
-          }
-          return bound(...args);
-        });
+        return retryOnPromise(() =>
+          qrl.resolve().then((fn) => {
+            if (!isFunction(fn)) {
+              throw qError(QError.qrlIsNotFunction);
+            }
+            return bound(...args);
path: packages/qwik/src/core/shared/qrl/qrl-class.ts
user: login: wmertens
body: Move the `retryOnPromise` here
created_at: 2025-04-03T09:09:08Z
author_association: MEMBER
diff_hunk: @@ -93,11 +93,13 @@ export const createQRL = <TYPE>(
     // Note that we bind the current `this`
     const bound = (...args: QrlArgs<TYPE>): ValueOrPromise<QrlReturn<TYPE> | undefined> => {
       if (!qrl.resolved) {
-        return qrl.resolve().then((fn) => {
+        //@ts-ignore
path: packages/qwik/src/core/shared/qrl/qrl-class.ts
user: login: Varixo
body: why this is needed?
created_at: 2025-04-06T14:25:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,5 @@
+---
+'@qwik.dev/core': major
+---
+
+FIX: Add a new e2e test to verify QRL behavior without uncaught promises. Introduce retry logic for QRL resolution to handle potential promise retries, ensuring robustness in asynchronous operations.
path: .changeset/sweet-hairs-remember.md
user: login: Varixo
body: this message will appear on patch notes, so it should contain information for developers. I think information about e2e test is not useful :D I would leave only the second sentence
created_at: 2025-04-06T14:27:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,5 @@
+---
+'@qwik.dev/core': major
path: .changeset/sweet-hairs-remember.md
user: login: Varixo
body: please change it to patch
created_at: 2025-04-06T14:27:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,5 @@
+---
+'@qwik.dev/core': patch
+---
+
+FIX: Introduce retry logic for QRL resolution to handle potential promise retries, ensuring robustness in asynchronous operations.
path: .changeset/sweet-hairs-remember.md
user: login: wmertens
body: 🤖😅👌
created_at: 2025-04-08T05:40:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,5 @@
+---
+'@qwik.dev/core': patch
+---
+
+FIX: Introduce retry logic for QRL resolution to handle potential promise retries, ensuring robustness in asynchronous operations.
path: .changeset/sweet-hairs-remember.md
user: login: JerryWu1234
body: what's problem here ?
created_at: 2025-04-08T05:41:59Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-01T03:08:16Z
body: ###  🦋  Changeset detected

Latest commit: 9d50c28363e8b52634e22b29c1fa8bfec933f960

**The changes in this PR will be included in the next version bump.**



Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/JerryWu1234/qwik/new/7442_fix_promise_fail?filename=.changeset/great-tools-impress.md&value=---%0A%22%40fake-scope%2Ffake-pkg%22%3A%20patch%0A---%0A%0A7442%20fix%20promise%20fail%0A)


author_association: NONE
user: login: pkg-pr-new[bot]
created_at: 2025-04-01T03:17:37Z
body: 
[Open in StackBlitz](https://pkg.pr.new/template/177cee08-b626-45c1-b6cc-7228c34ba09b)


```
npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/core@7470
```


```
npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/router@7470
```


```
npm i https://pkg.pr.new/QwikDev/qwik/eslint-plugin-qwik@7470
```


```
npm i https://pkg.pr.new/QwikDev/qwik/create-qwik@7470
```


_commit: <a href="https://github.com/QwikDev/qwik/runs/40076298296"><code>9d50c28</code></a>_

author_association: NONE
user: login: wmertens
created_at: 2025-04-03T09:31:43Z
body: weird, looks like it makes e2e hang :( does `pnpm test.e2e.chromium` work for you @JerryWu1234 ?
author_association: MEMBER
user: login: JerryWu1234
created_at: 2025-04-03T09:46:01Z
body: Yes. It's fine on my side. Let me try it on my own computer. But do it late
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-04T08:44:15Z
body: <!-- refined-cf-pages-action:deployment-summary:qwik -->

###### built with [Refined Cloudflare Pages Action](https://github.com/AdrianGonz97/refined-cf-pages-action)
## ⚡ Cloudflare Pages Deployment
| Name | Status | Preview | Last Commit |
| :--- | :----- | :------ | :---------- |
| **qwik-docs** | ✅ Ready ([View Log](https://github.com/QwikDev/qwik/actions/runs/14301217338)) | [Visit Preview](https://jerrywu1234-7442-fix-promise.qwik-8nx.pages.dev) | 9d50c28363e8b52634e22b29c1fa8bfec933f960 |

author_association: CONTRIBUTOR
user: login: JerryWu1234
created_at: 2025-04-07T02:58:10Z
body: @Varixo Now it's fine

author_association: CONTRIBUTOR