Skip to content

Conversation

@dummdidumm
Copy link
Member

@dummdidumm dummdidumm commented Oct 20, 2025

How to migrate

Instead of throwing with the passed in parameter, you now throw via the imported invalid function from @sveltejs/kit.

+ import { invalid } from '@sveltejs/kit';
import { form } from '$app/server';

export const myForm = form(
  someSchema,
-  (data, invalid) => {
+  (data, issue) => {
    invalid(
      'something went wrong',
-     invalid.some.field('message')
+     issue.some.field('message')
    );
  }
);

PR description

TypeScript kinda forced our hand here - due to limitations of control flow analysis it does not detect the never return type for anything else than functions that are used directly (i.e. passing a function as a parameter doesn't work unless you explicitly type it); see microsoft/TypeScript#36753 for more info.

This therefore changes invalid to be a function that you import just like redirect or error. A nice benefit of this is that you'll no longer have to use the second parameter passed to remote form functions to construct the list of issues in case you want to create an issue for the whole form and not just a specific field.

Closes #14745


Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • 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
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

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

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

TypeScript kinda forced our hand here - due to limitations of control flow analysis it does not detect the `never` return type for anything else than functions that are used directly (i.e. passing a function as a parameter doesn't work unless you explicitly type it); see microsoft/TypeScript#36753 for more info.

This therefore changes `invalid` to be a function that you import just like `redirect` or `error`. A nice benefit of this is that you'll no longer have to use the second parameter passed to remote form functions to construct the list of issues in case you want to create an issue for the whole form and not just a specific field.

Closes #14745
@changeset-bot
Copy link

changeset-bot bot commented Oct 20, 2025

🦋 Changeset detected

Latest commit: 0cb5edc

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

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit 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

Comment on lines +343 to +344
'invalid() should now be imported from @sveltejs/kit to throw validaition issues. ' +
'Keep using the parameter (now named issue) provided to the form function only to construct the issues, e.g. invalid(issue.field("message")). ' +
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
'invalid() should now be imported from @sveltejs/kit to throw validaition issues. ' +
'Keep using the parameter (now named issue) provided to the form function only to construct the issues, e.g. invalid(issue.field("message")). ' +
'`invalid` should now be imported from `@sveltejs/kit` to throw validation issues. ' +
'The second parameter provided to the form function (renamed to `issue`) is still used to construct issues, e.g. `invalid(issue.field(\'message\'))`. ' +

### Programmatic validation
In addition to declarative schema validation, you can programmatically mark fields as invalid inside the form handler using the `invalid` function. This is useful for cases where you can't know if something is valid until you try to perform some action:
In addition to declarative schema validation, you can programmatically mark fields as invalid inside the form handler using the `invalid` function. This is useful for cases where you can't know if something is valid until you try to perform some action. Just like `redirect` or `error`, `invalid` throws. It expects a list of standard-schema-compliant issues. Use the `issue` parameter for type-safe creation of such issues:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
In addition to declarative schema validation, you can programmatically mark fields as invalid inside the form handler using the `invalid` function. This is useful for cases where you can't know if something is valid until you try to perform some action. Just like `redirect` or `error`, `invalid` throws. It expects a list of standard-schema-compliant issues. Use the `issue` parameter for type-safe creation of such issues:
In addition to declarative schema validation, you can programmatically mark fields as invalid inside the form handler using the `invalid` function. This is useful for cases where you can't know if something is valid until you try to perform some action. Just like `redirect` or `error`, `invalid` throws. It expects a list of strings (for issues relating to the form as a whole) or standard-schema-compliant issues (for those relating to a specific field). Use the `issue` parameter for type-safe creation of such issues:

Comment on lines +477 to +481
// This will show up on the root issue list
'Purchase failed',
// Creates a `{ message: ..., path: ['qty'] }` object,
// will show up on the issue list for the `qty` field
issue.qty(`we don't have enough hotcakes`)
Copy link
Member

Choose a reason for hiding this comment

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

I know that the previous example didn't illustrate root-level issues, but I think that showing both like this makes things more confusing rather than less — it suggests that you're supposed to add a root-level issue alongside a field-level issue which I would definitely consider unusual. I think we're better off reverting, and relying on the prose above

Suggested change
// This will show up on the root issue list
'Purchase failed',
// Creates a `{ message: ..., path: ['qty'] }` object,
// will show up on the issue list for the `qty` field
issue.qty(`we don't have enough hotcakes`)
invalid.qty(`we don't have enough hotcakes`)

* ```ts
* import { invalid } from '@sveltejs/kit';
* import { form } from '$app/server';
* import * as v from 'valibot';
Copy link
Member

Choose a reason for hiding this comment

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

we can save ourselves some space by making this an import, and I think we should do 'login' rather than 'register' (reasons below)

Suggested change
* import * as v from 'valibot';
* import { tryLogin } from $lib/server/auth';
* import * as v from 'valibot';

Comment on lines +229 to +232
*
* function tryRegisterUser(name: string, password: string) {
* // ...
* }
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
*
* function tryRegisterUser(name: string, password: string) {
* // ...
* }

* // ...
* }
*
* export const register = form(
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* export const register = form(
* export const login = form(

Comment on lines +236 to +240
* async ({ name, _password }, issue) => {
* const success = tryRegisterUser(name, _password);
* if (!success) {
* invalid('Registration failed', issue.name('This username is already taken'));
* }
Copy link
Member

Choose a reason for hiding this comment

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

For the same reason as https://github.com/sveltejs/kit/pull/14768/files#r2444740445 I think we should avoid mixing and matching here — this example reads very much as though 'Registration failed' is the title of the issue and 'This username is already taken' is the detail. In reality you would only create the name issue.

Given that the suggestion shows the use of issue.foo(...), I think it makes sense to do the opposite here, and a classic example of a root-level issue is a login failure:

Suggested change
* async ({ name, _password }, issue) => {
* const success = tryRegisterUser(name, _password);
* if (!success) {
* invalid('Registration failed', issue.name('This username is already taken'));
* }
* async ({ name, _password }) => {
* const success = tryLogin(name, _password);
* if (!success) {
* invalid('Incorrect username or password');
* }

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.

Docs: Imperative invalidation caveat

3 participants