Skip to content

feat(core): defer AsyncSignal errors during render#8757

Open
wmertens wants to merge 2 commits into
build/v2from
async-defer-error-render
Open

feat(core): defer AsyncSignal errors during render#8757
wmertens wants to merge 2 commits into
build/v2from
async-defer-error-render

Conversation

@wmertens

Copy link
Copy Markdown
Member

this allows writing

{signal.value && ...}
{signal.error && ...}

Note that setting error does not clear .value so the stale value can still be shown.

@changeset-bot

changeset-bot Bot commented Jun 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c40e4b6

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

This PR includes changesets to release 5 packages
Name Type
@qwik.dev/core Minor
eslint-plugin-qwik Minor
@qwik.dev/react Minor
@qwik.dev/router Minor
create-qwik Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jun 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

@qwik.dev/core

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

@qwik.dev/router

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

eslint-plugin-qwik

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

create-qwik

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

@qwik.dev/optimizer

npm i https://pkg.pr.new/QwikDev/qwik/@qwik.dev/optimizer@8757

commit: c40e4b6

@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor
built with Refined Cloudflare Pages Action

⚡ Cloudflare Pages Deployment

Name Status Preview Last Commit
qwik-docs ✅ Ready (View Log) Visit Preview c477a6b

@Varixo Varixo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread packages/qwik/src/core/use/use-core.ts Outdated
Comment thread packages/qwik/src/core/reactive-primitives/impl/async-signal-impl.ts Outdated
Read `.value` when you need the result. If the result is not ready yet, that part of the UI can wait and a [`<Suspense>`](/docs/labs/suspense/index.mdx) boundary can show fallback UI.

Note: If the function errored, reading from `.value` will throw the error. This is intentional, so you can handle errors with a `<ErrorBoundary>`. To avoid this, check `.error` before reading `.value`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good

@wmertens wmertens force-pushed the async-defer-error-render branch from ecba0a3 to c477a6b Compare June 22, 2026 14:03
@wmertens wmertens marked this pull request as ready for review June 23, 2026 14:15
Copilot AI review requested due to automatic review settings June 23, 2026 14:15
@wmertens wmertens requested review from a team as code owners June 23, 2026 14:15
@maiieul maiieul moved this from In progress to Waiting For Review in Qwik Development Jun 23, 2026
@wmertens wmertens enabled auto-merge June 23, 2026 14:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR changes AsyncSignal error semantics during component rendering so that reading an errored async signal’s .value can be deferred until after render, allowing patterns like {signal.value && ...} and {signal.error && ...} without .value throwing mid-render.

Changes:

  • Add render-only error deferral tracking to the invoke context and enable it during executeComponent().
  • Defer AsyncSignalImpl.untrackedValue throwing during a render and rethrow after render if .error was not read.
  • Update API/runtime docs and add a regression test for the “read .value then .error” render pattern, plus a changeset.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/qwik/src/core/use/use-core.ts Adds per-render context field to track deferred async-signal errors.
packages/qwik/src/core/shared/component-execution.ts Enables/reset tracking during component execution and rethrows unread async-signal errors post-render.
packages/qwik/src/core/reactive-primitives/impl/async-signal-impl.ts Defers throwing from .value during render and marks .error reads as handled.
packages/qwik/src/core/reactive-primitives/signal.public.ts Documents the new render-only deferred-throw behavior for AsyncSignal.
packages/qwik/src/core/tests/use-async.spec.tsx Adds a test ensuring separate .value/.error reads in render don’t throw.
packages/docs/src/routes/docs/(qwik)/core/state/index.mdx Updates state docs around useAsync$() error behavior.
packages/docs/src/routes/api/qwik/index.mdx Updates API docs text for AsyncSignal to mention render deferral.
packages/docs/src/routes/api/qwik/api.json Regenerates API docs JSON content to include the updated AsyncSignal docs.
.changeset/async-signal-defer-error-read.md Declares a minor release for the new AsyncSignal render behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


Read `.value` when you need the result. If the result is not ready yet, that part of the UI can wait and a [`<Suspense>`](/docs/labs/suspense/index.mdx) boundary can show fallback UI.

Note: If the function errored, reading from `.value` will throw the error. This is intentional, so you can handle errors with a `<ErrorBoundary>`. To avoid this, check `.error` before reading `.value`.
Comment on lines +369 to 388
it('does not throw when .value and .error are read separately in a render', async () => {
const Counter = component$(() => {
const doubleCount = useAsync$(() => Promise.reject(new Error('boom')));
// Reading .value first must not throw, because the render also handles .error.
const value = doubleCount.value;
const error = doubleCount.error;
return <div>{error ? `error: ${error.message}` : value}</div>;
});
let threw = false;
let container;
try {
({ container } = await render(<Counter />, { debug }));
} catch {
threw = true;
}
// Reading .value of the errored signal did not throw, because .error was also read.
expect(threw).toBe(false);
expect(container!.element.querySelector('div')?.textContent).toBe('error: boom');
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Waiting For Review

Development

Successfully merging this pull request may close these issues.

4 participants