diff --git a/packages/solid/src/reactive/signal.ts b/packages/solid/src/reactive/signal.ts index 24929d0a0..803309c88 100644 --- a/packages/solid/src/reactive/signal.ts +++ b/packages/solid/src/reactive/signal.ts @@ -613,11 +613,12 @@ export function createResource( [value, setValue] = (options.storage || createSignal)(options.initialValue) as Signal< T | undefined >, - [error, setError] = createSignal(undefined), + [error, setError] = createSignal(undefined, { equals: false }), [track, trigger] = createSignal(undefined, { equals: false }), [state, setState] = createSignal<"unresolved" | "pending" | "ready" | "refreshing" | "errored">( resolved ? "ready" : "unresolved" - ); + ), + [pState, setPState] = createSignal<0 | 1 | 2>(0); if (sharedConfig.context) { id = `${sharedConfig.context.id}${sharedConfig.context.count++}`; @@ -625,11 +626,11 @@ export function createResource( if (options.ssrLoadFrom === "initial") initP = options.initialValue as T; else if (sharedConfig.load && (v = sharedConfig.load(id))) initP = v; } - function loadEnd(p: Promise | null, v: T | undefined, error?: any, key?: S) { + function loadEnd(p: Promise | null, isSuccess: boolean, v: any, key?: S) { if (pr === p) { pr = null; key !== undefined && (resolved = true); - if ((p === initP || v === initP) && options.onHydrated) + if ((p === initP || (isSuccess && v === initP)) && options.onHydrated) queueMicrotask(() => options.onHydrated!(key, { value: v })); initP = NO_INIT; if (Transition && p && loadedUnderTransition) { @@ -637,17 +638,21 @@ export function createResource( loadedUnderTransition = false; runUpdates(() => { Transition!.running = true; - completeLoad(v, error); + completeLoad(isSuccess, v); }, false); - } else completeLoad(v, error); + } else completeLoad(isSuccess, v); } - return v; + if (isSuccess) return v; + // TODO why aren't we rethrowing + return undefined; } - function completeLoad(v: T | undefined, err: any) { + function completeLoad(isSuccess: boolean, v: any) { runUpdates(() => { - if (err === undefined) setValue(() => v); - setState(err !== undefined ? "errored" : resolved ? "ready" : "unresolved"); - setError(err); + setState(!isSuccess ? "errored" : resolved ? "ready" : "unresolved"); + setPState(isSuccess ? 1 : 2); + if (isSuccess) { + setValue(() => v); + } else setError(() => v); for (const c of contexts.keys()) c.decrement!(); contexts.clear(); }, false); @@ -657,7 +662,7 @@ export function createResource( const c = SuspenseContext && useContext(SuspenseContext), v = value(), err = error(); - if (err !== undefined && !pr) throw err; + if (pState() === 2 && !pr) throw err; if (Listener && !Listener.user && c) { createComputed(() => { track(); @@ -678,7 +683,7 @@ export function createResource( const lookup = dynamic ? dynamic() : (source as S); loadedUnderTransition = Transition && Transition.running; if (lookup == null || lookup === false) { - loadEnd(pr, untrack(value)); + loadEnd(pr, true, untrack(value)); return; } if (Transition && pr) Transition.promises.delete(pr); @@ -692,24 +697,24 @@ export function createResource( }) ); if (!isPromise(p)) { - loadEnd(pr, p, undefined, lookup); + loadEnd(pr, true, p, lookup); return p; } pr = p; if ("value" in p) { - if ((p as any).status === "success") loadEnd(pr, p.value as T, undefined, lookup); - else loadEnd(pr, undefined, undefined, lookup); + loadEnd(pr, (p as any).status === "success", p.value, lookup); return p; } scheduled = true; queueMicrotask(() => (scheduled = false)); runUpdates(() => { + setPState(0); setState(resolved ? "refreshing" : "pending"); trigger(); }, false); return p.then( - v => loadEnd(p, v, undefined, lookup), - e => loadEnd(p, undefined, castError(e), lookup) + v => loadEnd(p, true, v, lookup), + e => loadEnd(p, false, e, lookup) ) as Promise; } Object.defineProperties(read, { @@ -725,7 +730,7 @@ export function createResource( get() { if (!resolved) return read(); const err = error(); - if (err && !pr) throw err; + if (pState() === 2 && !pr) throw err; return value(); } } @@ -1683,11 +1688,6 @@ function reset(node: Computation, top?: boolean) { } } -function castError(err: unknown): Error { - if (err instanceof Error) return err; - return new Error(typeof err === "string" ? err : "Unknown error", { cause: err }); -} - function runErrors(err: unknown, fns: ((err: any) => void)[], owner: Owner | null) { try { for (const f of fns) f(err); @@ -1698,17 +1698,16 @@ function runErrors(err: unknown, fns: ((err: any) => void)[], owner: Owner | nul function handleError(err: unknown, owner = Owner) { const fns = ERROR && owner && owner.context && owner.context[ERROR]; - const error = castError(err); - if (!fns) throw error; + if (!fns) throw err; if (Effects) Effects!.push({ fn() { - runErrors(error, fns, owner); + runErrors(err, fns, owner); }, state: STALE } as unknown as Computation); - else runErrors(error, fns, owner); + else runErrors(err, fns, owner); } function resolveChildren(children: JSX.Element | Accessor): ResolvedChildren { diff --git a/packages/solid/src/render/flow.ts b/packages/solid/src/render/flow.ts index 1b6f329b6..0d3009a00 100644 --- a/packages/solid/src/render/flow.ts +++ b/packages/solid/src/render/flow.ts @@ -256,25 +256,37 @@ export function ErrorBoundary(props: { fallback: JSX.Element | ((err: any, reset: () => void) => JSX.Element); children: JSX.Element; }): JSX.Element { - let err; - if (sharedConfig!.context && sharedConfig!.load) - err = sharedConfig.load(sharedConfig.context.id + sharedConfig.context.count); + let err, + hasError = false; + if (sharedConfig.context && sharedConfig.load && sharedConfig.has) { + const key = sharedConfig.context.id + sharedConfig.context.count; + hasError = sharedConfig.has(key); + err = sharedConfig.load(key); + } const [errored, setErrored] = createSignal( err, - "_SOLID_DEV_" ? { name: "errored" } : undefined + "_SOLID_DEV_" ? { name: "errored", equals: false } : { equals: false } ); + const pushError = (action: any) => { + hasError = true; + setErrored(() => action); + }; + const clearError = () => { + hasError = false; + setErrored(); + }; Errors || (Errors = new Set()); - Errors.add(setErrored); - onCleanup(() => Errors.delete(setErrored)); + Errors.add(clearError as Setter); + onCleanup(() => Errors.delete(clearError as Setter)); return createMemo( () => { - let e: any; - if ((e = errored())) { + const e = errored(); + if (hasError) { const f = props.fallback; if ("_SOLID_DEV_" && (typeof f !== "function" || f.length == 0)) console.error(e); - return typeof f === "function" && f.length ? untrack(() => f(e, () => setErrored())) : f; + return typeof f === "function" && f.length ? untrack(() => f(e, clearError)) : f; } - return catchError(() => props.children, setErrored); + return catchError(() => props.children, pushError); }, undefined, "_SOLID_DEV_" ? { name: "value" } : undefined diff --git a/packages/solid/src/server/reactive.ts b/packages/solid/src/server/reactive.ts index 5bdf28284..9ede454a8 100644 --- a/packages/solid/src/server/reactive.ts +++ b/packages/solid/src/server/reactive.ts @@ -13,18 +13,13 @@ export type Setter = undefined extends T export type Signal = [get: Accessor, set: Setter]; const ERROR = Symbol("error"); -export function castError(err: unknown): Error { - if (err instanceof Error) return err; - return new Error(typeof err === "string" ? err : "Unknown error", { cause: err }); -} function handleError(err: unknown, owner = Owner): void { const fns = owner && owner.context && owner.context[ERROR]; - const error = castError(err); - if (!fns) throw error; + if (!fns) throw err; try { - for (const f of fns) f(error); + for (const f of fns) f(err); } catch (e) { handleError(e, (owner && owner.owner) || null); } diff --git a/packages/solid/src/server/rendering.ts b/packages/solid/src/server/rendering.ts index caf7fc76f..5a7fce4e7 100644 --- a/packages/solid/src/server/rendering.ts +++ b/packages/solid/src/server/rendering.ts @@ -8,7 +8,6 @@ import { Accessor, Setter, Signal, - castError, cleanNode, createOwner } from "./reactive.js"; @@ -258,6 +257,7 @@ export function ErrorBoundary(props: { children: string; }) { let error: any, + hasError = false, res: any, clean: any, sync = true; @@ -275,13 +275,14 @@ export function ErrorBoundary(props: { return catchError( () => (res = props.children), err => { + hasError = true; error = err; !sync && ctx.replace("e" + id, displayFallback); sync = true; } ); }); - if (error) return displayFallback(); + if (hasError) return displayFallback(); sync = false; return { t: `${resolveSSRNode(res)}` }; } @@ -368,7 +369,8 @@ export function createResource( let resource: { ref?: any; data?: T } = {}; let value = options.storage ? options.storage(options.initialValue)[0]() : options.initialValue; let p: Promise | T | null; - let error: any; + let error: any, + hasError = false; if (sharedConfig.context!.async && options.ssrLoadFrom !== "initial") { resource = sharedConfig.context!.resources[id] || (sharedConfig.context!.resources[id] = {}); if (resource.ref) { @@ -378,7 +380,7 @@ export function createResource( } } const read = () => { - if (error) throw error; + if (hasError) throw error; if (resourceContext && p) resourceContext.push(p!); const resolved = options.ssrLoadFrom !== "initial" && @@ -436,7 +438,8 @@ export function createResource( .catch(err => { read.loading = false; read.state = "errored"; - read.error = error = castError(err); + read.error = error = err; + hasError = true; p = null; notifySuspense(contexts); throw error; diff --git a/packages/solid/test/resource.spec.ts b/packages/solid/test/resource.spec.ts index ef0b5e447..35c4927a1 100644 --- a/packages/solid/test/resource.spec.ts +++ b/packages/solid/test/resource.spec.ts @@ -66,10 +66,14 @@ describe("Simulate a dynamic fetch", () => { expect(value.error).toBeUndefined(); reject("Because I said so"); await Promise.resolve(); - expect(error).toBeInstanceOf(Error); - expect(error.message).toBe("Because I said so"); - expect(value.error).toBeInstanceOf(Error); - expect(value.error.message).toBe("Because I said so"); + // expect(error).toBeInstanceOf(Error); + // expect(error.message).toBe("Because I said so"); + // expect(value.error).toBeInstanceOf(Error); + // expect(value.error.message).toBe("Because I said so"); + expect(error).toBeTypeOf("string"); + expect(error).toBe("Because I said so"); + expect(value.error).toBeTypeOf("string"); + expect(value.error).toBe("Because I said so"); expect(value.loading).toBe(false); }); }); @@ -204,7 +208,7 @@ describe("using Resource with errors", () => { reject(null); await Promise.resolve(); expect(value.state === "errored").toBe(true); - expect(value.error.message).toBe("Unknown error"); + expect(value.error).toBeNull(); }); });