Skip to content

Conversation

LeeWxx
Copy link
Contributor

@LeeWxx LeeWxx commented Oct 2, 2025

Fixes #16850

Starting in Svelte 5.38.2, guard expressions such as {#if centerRow && centerRow.depth} could end up evaluating $.get(centerRow) multiple times within a single expression. This meant that the first null check and the subsequent centerRow.depth access might see different values, which could cause a Cannot read properties of undefined error. The fix changes build_expression so that it reads a state or derived signal exactly once per expression, caches that intermediate result, and then reuses it, while deliberately leaving reads inside nested functions untouched so that existing behavior continues to work as before. In addition, the IfBlock visitor was updated to compute the guard expression up front so that any nested blocks share the memoized value. If there is a more robust or idiomatic way to handle this in Svelte, I would very much appreciate any feedback or suggestions.

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code within packages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

Copy link

changeset-bot bot commented Oct 2, 2025

🦋 Changeset detected

Latest commit: a758384

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

This PR includes changesets to release 1 package
Name Type
svelte Patch

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

@svelte-docs-bot
Copy link

Copy link
Contributor

github-actions bot commented Oct 2, 2025

Playground

pnpm add https://pkg.pr.new/svelte@16881

walk(value, null, {
MemberExpression(node) {
if (node.object.type === 'Identifier' && memoized.has(node.object.name)) {
node.optional = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

I have a suspicion that this is what "fixes" the problem by turning

 centerRow.depth != undefined && centerRow.depth > 0

into

centerRow_value?.depth != undefined && centerRow_value?.depth > 0

which is not really equal.

Copy link
Contributor Author

@LeeWxx LeeWxx Oct 2, 2025

Choose a reason for hiding this comment

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

@7nik Thanks for double-checking! At first glance it might look a bit different, but I think Svelte still seems to emit the leading guard

return (
  centerRow_value != undefined &&
  centerRow_value?.depth != undefined &&
  centerRow_value?.depth > 0
);

From what I can tell, the optional chaining only comes into play after we already know that centerRow_value isn’t null or undefined. In that case, centerRow_value?.depth would effectively behave the same as centerRow_value.depth. The ?. just seems to act as a safety net, in case the value somehow changes between reads while reusing the cached value. So I believe the semantics of the guard essentially remain the same.

Copy link
Contributor

Choose a reason for hiding this comment

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

Look at this variant - it fails on this PR as well. The problem is that the nested if / template effect / etc runs when it shouldn't.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@7nik Oops, you’re right

it looks like this new variant still slips through what I’ve tried so far. From what I can tell, the inner template effect is still calling $.get(centerRow) even after the guard has already short-circuited, which makes the block run when it really shouldn’t.

I see two possible ways we could approach this. One would be to hoist the guard result so that the entire block (effects and nested branches included) reuses the same snapshot. The other would be to change how the template effect checks for guard failure before it runs.

Do you think one of these directions would fit better?

Copy link
Contributor

Choose a reason for hiding this comment

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

The outer if should execute first and destroy the branch, thus no execution of centerRow.depth when centerRow is undefined. But I'm not so familiar with the reactivity internals (especially async ones) to say why effects are executed in another order and how to fix it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@7nik Thanks for catching that earlier variant! I hoisted the guard value so the entire block (including template effects and nested branches) reuses the same snapshot.
If you’re aware of other guard scenarios we should test, I’d really appreciate the pointers.

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.

Guard check on variable doesn't work as expected
2 participants