Skip to content

fix(qwik): guard undefined vNode in scheduleTask and scheduleEffects#8559

Open
juslintek wants to merge 5 commits intoQwikDev:rolldown-vite-supportfrom
juslintek:fix/guard-undefined-vnode-in-mark-dirty
Open

fix(qwik): guard undefined vNode in scheduleTask and scheduleEffects#8559
juslintek wants to merge 5 commits intoQwikDev:rolldown-vite-supportfrom
juslintek:fix/guard-undefined-vnode-in-mark-dirty

Conversation

@juslintek
Copy link
Copy Markdown

@juslintek juslintek commented Apr 15, 2026

What is it?

  • Bug fix

Description

Fixes a runtime crash during hydration where markVNodeDirty receives an undefined vNode, causing:

TypeError: Cannot read properties of undefined (reading 'dirty')
  at markVNodeDirty (@qwik.dev/core/dist/core.mjs:6727:29)
  at String.scheduleTask (@qwik.dev/core/dist/core.mjs:8788:5)

This crash makes the entire application non-functional — no page renders, no login, all routes are dead.

Root Cause

scheduleTask (in use-task.ts) and scheduleEffects (in reactive-primitives/utils.ts) pass task.$el$ / consumer.$el$ to markVNodeDirty without checking if the host element is defined.

task.$el$ can be undefined when:

  • The container is destroyed during async dispatch (the qwikloader's dispatch is now async, and if a navigation/destruction happens during an await, the container's $stateData$ is truncated, causing subsequent deserialization to return undefined for VNode references)
  • The serialized Task array has fewer elements than expected during inflation

Other callers of markVNodeDirty already have this guard — for example, wrapped-signal-impl.ts checks if (this.$container$ && this.$hostElement$) before calling markVNodeDirty. This PR adds the same defensive pattern to the two unguarded call sites.

Changes

packages/qwik/src/core/use/use-task.ts

  • Added null check for task?.$el$ in scheduleTask before calling markVNodeDirty

packages/qwik/src/core/reactive-primitives/utils.ts

  • Added null check for consumer.$el$ in the isTask(consumer) branch of scheduleEffects before calling markVNodeDirty

packages/qwik/src/core/use/use-task.unit.ts

  • Added three test cases: two verifying scheduleTask does not throw when task.$el$ or _captures[0] is undefined, and one positive test verifying markVNodeDirty IS called when task.$el$ is defined
  • Tests use module-level vi.mock with per-test Object.defineProperty to avoid the Vitest hoisting trap

.changeset/gentle-tasks-guard.md

  • Patch changeset for @qwik.dev/core

Docs

No user-facing documentation changes needed — scheduleTask, scheduleEffects, and markVNodeDirty are internal APIs not exposed in the public docs or GLOSSARY.

Checklist

  • My code follows the developer guidelines of this project
  • I performed a self-review of my own code
  • I added a changeset with pnpm change
  • I made corresponding changes to the Qwik docs (N/A — internal APIs only)
  • I added new tests to cover the fix / functionality

When the qwikloader dispatches events asynchronously, the Task's host
element ($el$) can be undefined if the container was destroyed during
async dispatch or if deserialization produced an incomplete Task object.

This causes `markVNodeDirty` to crash with:
  TypeError: Cannot read properties of undefined (reading 'dirty')

Add null checks for `task.$el$` / `consumer.$el$` before calling
`markVNodeDirty`, consistent with the guard already present in
`wrapped-signal-impl.ts`.
Verify that scheduleTask does not throw when task.$el$ is undefined
or when the task itself is not properly deserialized.
@juslintek juslintek requested a review from a team as a code owner April 15, 2026 14:16
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 15, 2026

🦋 Changeset detected

Latest commit: efa3221

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.

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4157811b77

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/qwik/src/core/use/use-task.unit.ts Outdated
Move vi.mock calls to module level (where Vitest hoists them) and use
Object.defineProperty to set _captures per-test. This avoids the
closure-over-test-local-variables issue flagged in review.

Also adds a positive test verifying markVNodeDirty IS called when
task.$el$ is defined.
Copy link
Copy Markdown
Member

@Varixo Varixo left a comment

Choose a reason for hiding this comment

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

Hello! Thank you for the PR.
Unfortunately for me it looks more like workaround instead of real solution. Basically task should have always host element. If there is a situation when it doesn't then its already a bug imo. Also the test case should show real example when useTask$ will have no host element.

Additionally the rolldown support branch is a bit behind the v2 branch, so first we need to check if this is fixed.

@juslintek
Copy link
Copy Markdown
Author

Hello! Thank you for the PR. Unfortunately for me it looks more like workaround instead of real solution. Basically task should have always host element. If there is a situation when it doesn't then its already a bug imo. Also the test case should show real example when useTask$ will have no host element.

Additionally the rolldown support branch is a bit behind the v2 branch, so first we need to check if this is fixed.

Hi, thanks for review. Should I attempt a rebase of parent branch with this fix in a separate branch to validate whether it is working or not?

Regarding test I will create real example of my current project that I am working with this version of qwik.

…ing-async-dispatch scenario

Addresses review feedback from @Varixo — tests now simulate the actual
DomContainer.$destroy$() flow where $getObjectById$ returns undefined
after container destruction during async qwikloader dispatch, rather
than artificially constructing Tasks with undefined host elements.

The tests mock getDomContainer (not qrl-class internals) and exercise
the real deserializeCaptures → $getObjectById$ → undefined path that
causes the crash in production.
@Varixo
Copy link
Copy Markdown
Member

Varixo commented Apr 16, 2026

Hello! Thank you for the PR. Unfortunately for me it looks more like workaround instead of real solution. Basically task should have always host element. If there is a situation when it doesn't then its already a bug imo. Also the test case should show real example when useTask$ will have no host element.
Additionally the rolldown support branch is a bit behind the v2 branch, so first we need to check if this is fixed.

Hi, thanks for review. Should I attempt a rebase of parent branch with this fix in a separate branch to validate whether it is working or not?

Regarding test I will create real example of my current project that I am working with this version of qwik.

The bug shouldn't be related to rolldown support, so you can just fix it on the build/v2 branch. Thank you for your work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants