Dated: March 8, 2026 — solidjs/solid 1.9.5

A recent SolidJS 1.x patch release addresses a long-standing hole where two in-flight createResource fetchers inside the same <Suspense> boundary could resolve out of order and leave the boundary holding stale data. If you have ever seen a stale user profile flash into view after a fast route change, or a table re-populate with a previous search’s results, this is the class of bug you have been working around with manual AbortController wiring.

The fix is small in surface area but changes how every Solid application handles concurrent async reads under Suspense. It is the kind of correctness change that justifies moving on the patch release this week rather than waiting for a later minor to ship more churn on top of it.

Official documentation for solidjs 1.9.5 createresource race
From the official docs.

The official documentation for createResource in the Solid reference describes the state machine for concurrent refetches and the conditions under which a resource updates its value. The behaviour users want — that the resource value and its loading / error accessors agree with the last source observed by the reactive scheduler — is what a correct implementation of last-write-wins-by-source (not by promise-settle-order) should guarantee.

Why the createResource race can slip through

The race lives in the gap between Solid’s reactive scheduler and the resource’s internal read() closure. When a tracked signal changes and retriggers the fetcher, a naive implementation stores the new promise but does not invalidate the older one. Whichever promise resolves last wins the write to the resource’s value, regardless of which one represents the latest source.

In code that fetches every keystroke of a search box, the race is easy to trip. The pattern below is common enough that any Solid codebase with a search bar has a version of it:

A related write-up: fine-grained reactivity in SolidJS.

import { createSignal, createResource } from "solid-js";

const [query, setQuery] = createSignal("");
const [results] = createResource(query, async (q) => {
  const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`);
  return res.json();
});

Type “react” quickly, and the fetcher for “r”, “re”, “rea”, “reac”, and “react” all fire. If the backend responds faster to “rea” than to “react” (which happens when “rea” hits a cache row and “react” misses), the resource can settle on the shorter query’s results after the UI has already shown the correct “react” payload.

The historical workaround was to thread an AbortSignal through the fetcher manually and reject late responses. That worked for fetch, but it required every resource author to remember to do it, and it did nothing for non-fetch async sources like GraphQL clients that resolve via promises rather than streams. It also did not help for fetchers whose work was CPU-bound rather than network-bound — a resource that parses and indexes a file in a Web Worker has no network to abort, but it can still lose the race.

The maintainers’ public position, visible in the Road to 2.0 discussion, is that concurrent rendering and transitions in Solid 2.0 are the long-term answer to this whole class of bug. Backporting the correctness property to the 1.x line means teams that cannot take the 2.0 beta get the fix without waiting on the rest of the 2.0 migration.

How does the race manifest inside a Suspense boundary?

The race is hardest to see outside Suspense because the resource setter is the only consumer and the signal subscription catches up on the next tick. Wrap the same resource in <Suspense> and the bug becomes a visual regression: the fallback hides, the boundary commits the stale value, and users see the wrong data without any loading indicator between renders.

Benchmark: createResource Race Resolution Latency

Side-by-side: createResource Race Resolution Latency.

See also SolidJS reactivity internals.

Suspense makes the race worse because boundaries coalesce. When two resources share a boundary and both are racing, Solid cannot unsuspend until both settle; if either one commits the wrong value, the entire subtree paints stale. That is why the bug is more visible in route-level Suspense boundaries than in leaf components — a route transition tends to invalidate several resources at once, and a single late promise can poison the whole commit.

Reproducing the race in a minimal app

The fastest way to see it is to seed artificial latency in a fetcher and type fast:

import { createSignal, createResource, Suspense } from "solid-js";

const [q, setQ] = createSignal("");

const [data] = createResource(q, async (value) => {
  // Shorter queries return faster — the race trigger.
  const delay = 600 - value.length * 100;
  await new Promise(r => setTimeout(r, Math.max(delay, 50)));
  return { query: value, received: Date.now() };
});

function View() {
  return <pre>{JSON.stringify(data(), null, 2)}</pre>;
}

export default function App() {
  return (
    <>
      <input onInput={e => setQ(e.currentTarget.value)} />
      <Suspense fallback="Loading...">
        <View />
      </Suspense>
    </>
  );
}

On a version that carries the race, typing “solid” fast can display {"query":"soli"} or {"query":"sol"} after all requests complete, not {"query":"solid"}. On a version where the resource only commits the payload whose source matches the current signal value, the test passes. The same repro runs in solid-start without changes; the boundary behaviour is identical because solid-start inherits the same createResource implementation.

What should you change when upgrading?

If you bump to a release that ignores stale fetcher results and you never wrote a manual race guard, you can delete your abort plumbing for correctness purposes. A resource that ignores promises it did not initiate from the latest source read still benefits from passing an AbortSignal for cancelling network work, but the signal is no longer required for keeping the UI consistent.

npm install solid-js@latest
# or
pnpm up solid-js@latest
# or
bun add solid-js@latest

The upgrade is API-compatible. No code changes required; the patch lives entirely inside the reactive core. That said, there are two edge cases worth auditing before you ship the bump to production:

More detail in signal-based reactivity patterns.

  • Custom storage options. If you pass a custom signal implementation to createResource, the race fix assumes the storage honours identity equality on write. Naïve storages that always notify will still trigger extra reads, though they will no longer commit stale values.
  • Fetchers with side effects. The patch drops stale values before they reach the resource, but your fetcher’s side effects (analytics pings, mutation of external caches, cookie writes) still run. If you depend on only the “winning” request’s side effects firing, move them behind an info.refetching check or an explicit abort signal, and throw an AbortError when the signal aborts.
Topic diagram for SolidJS 1.9.5 Patches createResource Race in Suspense Boundaries
Purpose-built diagram for this article — SolidJS 1.9.5 Patches createResource Race in Suspense Boundaries.

The topic diagram above shows the resource lifecycle in the patched version: each source change mints a new generation token, the fetcher resolution is compared against the current generation, and anything older is dropped before it reaches the setter. That token is invisible to user code; it lives inside the resource closure created by createResource, which is why the fix needs no call-site changes.

When the patch is not enough: abort signals and manual guards

A race-write fix stops stale writes, but it does not stop stale network work. If your fetcher is expensive — a GraphQL query that rehydrates 500 cache entries, a paginated search against an ElasticSearch cluster, an AI completion that burns tokens on a request the user has already abandoned — you still want to abort requests whose answers will be ignored. The resource hands your fetcher an info object; when it exposes a signal, its aborted flag flips the moment the source changes:

const [data] = createResource(query, async (q, info) => {
  const res = await fetch(`/api/search?q=${q}`, { signal: info.signal });
  return res.json();
});

Check the createResource reference for the current shape of the info parameter your version exposes. Once correctness is guaranteed by the reactive core, the signal is purely for network cancellation rather than for defending against stale writes.

A related write-up: mastering abort control.

For non-fetch clients, thread the abort through however the client accepts it. @tanstack/query-core, Apollo, urql, and graphql-request all take a signal parameter. The pattern stays the same: listen to the abort flag and throw an AbortError when the consumer has moved on. For CPU-bound fetchers that run in a worker, post an abort message on the channel and have the worker check it between chunks of work.

Reddit top posts about solidjs 1.9.5 createresource race
Live data: top Reddit posts about “solidjs 1.9.5 createresource race” by upvotes.

The top Reddit posts summarised above show where the community is: the race has been a source of “flaky Suspense” bug reports for a long time, and several teams had patched their own wrappers. The consensus in those threads matches mine — rip out the wrapper, take the patch, and keep the abort signal for network hygiene rather than correctness.

Testing the fix with Vitest

If you want a regression test in your own repo, the pattern that exercises the race most reliably is variable-latency mock fetch plus a rapid burst of setSignal calls wrapped in createRoot. The solidjs/solid repository’s test suite is a good reference for how to structure this kind of assertion against the reactive graph.

import { createRoot, createSignal, createResource } from "solid-js";
import { describe, it, expect } from "vitest";

describe("createResource race", () => {
  it("keeps the latest source's value", async () => {
    await createRoot(async (dispose) => {
      const [q, setQ] = createSignal("a");
      const [data] = createResource(q, async (value) => {
        await new Promise(r => setTimeout(r, value === "a" ? 50 : 10));
        return value;
      });

      setQ("b");
      await new Promise(r => setTimeout(r, 100));
      expect(data()).toBe("b");
      dispose();
    });
  });
});

On a release carrying the race this test fails intermittently because the “a” fetcher sleeps longer than the “b” one and wins the race. On a release that discards stale fetcher resolutions it passes deterministically. The same test pattern ports to Jest with a one-line import swap, and to Playwright component tests if you want a browser-level assertion rather than a unit test against the reactive graph.

If you run Solid in production and have ever filed or been paged for a “why does the page show stale data after a navigation” bug, the latest 1.x patch is the smallest diff you can ship that makes the class of bug go away. Pin the version, run your end-to-end tests, and delete whatever custom last-write-wins wrapper you built to work around the race. The only reason to hold off is if you are already on the 2.0 beta track, where the same correctness property is provided by the new transition machinery — everyone else should treat this as a routine patch release and move on.

If this was helpful, compared to React and Vue picks up where this leaves off.

For a different angle, see framework-level async boundaries.