Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/diagnostic-code-prefix.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"solid-js": patch
---

Diagnostic messages now include their stable code identifier as a prefix (e.g. `[NO_OWNER_EFFECT] Effects created outside a reactive context will never be disposed`). Applied to all dev-mode diagnostics: `STRICT_READ_UNTRACKED`, `PENDING_ASYNC_UNTRACKED_READ`, `PENDING_ASYNC_FORBIDDEN_SCOPE`, `SIGNAL_WRITE_IN_OWNED_SCOPE`, `RUN_WITH_DISPOSED_OWNER`, `NO_OWNER_CLEANUP`, `CLEANUP_IN_FORBIDDEN_SCOPE`, `NO_OWNER_EFFECT`, `NO_OWNER_BOUNDARY`, `ASYNC_OUTSIDE_LOADING_BOUNDARY`, and `MISSING_EFFECT_FN`.
Diagnostic messages now include their stable code identifier as a prefix (e.g. `[NO_OWNER_EFFECT] Effects created outside a reactive context will never be disposed`). Applied to all dev-mode diagnostics: `STRICT_READ_UNTRACKED`, `PENDING_ASYNC_UNTRACKED_READ`, `PENDING_ASYNC_FORBIDDEN_SCOPE`, `SIGNAL_WRITE_IN_OWNED_SCOPE`, `RUN_WITH_DISPOSED_OWNER`, `NO_OWNER_CLEANUP`, `CLEANUP_IN_FORBIDDEN_SCOPE`, `NO_OWNER_EFFECT`, `NO_OWNER_BOUNDARY`, and `ASYNC_OUTSIDE_LOADING_BOUNDARY`.

The previously bare `throw new Error("Cannot create reactive primitives inside createTrackedEffect or owner-backed onSettled")` (raised when creating a memo, effect, or owner inside `createTrackedEffect`/`onSettled`) is now also surfaced through the diagnostic system as `PRIMITIVE_IN_FORBIDDEN_SCOPE` (severity `error`, dev-only, throws after emitting). Existing tests that match the message substring continue to work.

Expand Down
7 changes: 2 additions & 5 deletions .changeset/missing-effect-fn-loud-failure.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@
"solid-js": patch
---

`createEffect(compute)` (single-argument form) is now a hard error. Solid 2.0's `createEffect` requires a separate effect callback as its second argument: `createEffect(() => signal(), value => doWork(value))`.
Remove the unsupported `createEffect(compute)` overload. Solid 2.0 requires a separate effect callback as its second argument: `createEffect(() => signal(), value => doWork(value))`.

Two layers now surface the misuse:

- **TypeScript** — a deprecated overload `createEffect(compute): never` is added so editors render the call with strikethrough and surface the migration message on hover.
- **Runtime (dev)** — calling without an effect function now throws synchronously with a clear message and emits a new `MISSING_EFFECT_FN` diagnostic (replaces the previous opaque `TypeError: Cannot read properties of undefined`).
TypeScript now rejects single-argument calls instead of accepting them through a deprecated `never` overload. The dedicated development-only `MISSING_EFFECT_FN` diagnostic has also been removed; JavaScript callers receive the same runtime failure in development and production.

If you want a derived value, use `createMemo`. If you want a one-shot side effect at construction time, just call the function directly.
1 change: 0 additions & 1 deletion packages/signals/src/core/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export type DiagnosticCode =
| "ASYNC_OUTSIDE_LOADING_BOUNDARY"
| "INVALID_REFRESH_TARGET"
| "INVALID_AFFECTS_TARGET"
| "MISSING_EFFECT_FN"
| "SYNC_NODE_RECEIVED_ASYNC"
| "REACTIVITY_HALTED"
| "INVARIANT_VIOLATION"
Expand Down
30 changes: 0 additions & 30 deletions packages/signals/src/signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,37 +494,7 @@ export function createEffect<T>(
compute: ComputeFunction<undefined | NoInfer<T>, T>,
effectFn: EffectFunction<NoInfer<T>, T> | EffectBundle<NoInfer<T>, T>,
options?: EffectOptions
): void;
/**
* @deprecated `createEffect(compute)` (single argument) is no longer supported.
* Pass a separate effect function as the second argument:
* `createEffect(compute, effect)`. See [MISSING_EFFECT_FN].
*
* - For a side effect that reacts to changes, split the work:
* `createEffect(() => signal(), value => doWork(value))`.
* - For a derived value, use `createMemo(() => signal())`.
* - For a one-shot side effect at construction time, just call the function.
*/
export function createEffect<T>(compute: ComputeFunction<undefined | NoInfer<T>, T>): never;
export function createEffect<T>(
compute: ComputeFunction<undefined | NoInfer<T>, T>,
effectFn?: EffectFunction<NoInfer<T>, T> | EffectBundle<NoInfer<T>, T>,
options?: EffectOptions
): void {
if (__DEV__ && effectFn === undefined) {
const message =
"[MISSING_EFFECT_FN] createEffect requires both a compute function and an effect function. " +
"Use `createEffect(() => signal(), value => doWork(value))`. " +
"If you want a derived value, use `createMemo`. " +
"If you want a one-shot side effect, just call the function directly.";
emitDiagnostic({
code: "MISSING_EFFECT_FN",
kind: "lifecycle",
severity: "error",
message
});
throw new Error(message);
}
effect(compute as any, (effectFn as any).effect || effectFn, (effectFn as any).error, {
user: true,
...(__DEV__ ? { ...options, name: options?.name ?? "effect" } : options)
Expand Down
16 changes: 0 additions & 16 deletions packages/signals/tests/diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,22 +187,6 @@ describe("diagnostics", () => {
expect(events[0].kind).toBe("lifecycle");
});

it("emits a diagnostic and throws when createEffect is called without an effect function", () => {
const capture = DEV!.diagnostics.capture();

createRoot(() => {
expect(() => createEffect(() => 1)).toThrow(
/createEffect requires both a compute function and an effect function/
);
});

const events = capture.stop();
expect(events).toHaveLength(1);
expect(events[0].code).toBe("MISSING_EFFECT_FN");
expect(events[0].severity).toBe("error");
expect(events[0].kind).toBe("lifecycle");
});

it("emits a diagnostic before throwing on reactive primitive creation in a forbidden scope", () => {
const capture = DEV!.diagnostics.capture();

Expand Down
6 changes: 0 additions & 6 deletions packages/solid/skills/reactivity-diagnostics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,6 @@ stored past its lifetime — re-capture the owner at call time or guard with

## API misuse

### MISSING_EFFECT_FN

`createEffect(compute)` with a single argument is not supported. Split the
work: `createEffect(() => signal(), value => doWork(value))`. For a derived
value use `createMemo`; for a one-shot side effect just call the function.

### PRIMITIVE_IN_FORBIDDEN_SCOPE

Reactive primitives cannot be created inside `createTrackedEffect` or
Expand Down
3 changes: 2 additions & 1 deletion packages/solid/test/signals.type-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ createEffect(
() => {}
);

const _effectDeprecatedReturn: never = createEffect(() => 1);
// @ts-expect-error createEffect requires a separate effect function
createEffect(() => 1);
const _effectCanonicalReturn: void = createEffect(
() => 1,
() => {}
Expand Down