Repository: TanStack/form (2025-08-03T16:58:03Z)

Issue:
`submitMeta` is not passed to `onSubmitInvalid`
### Describe the bug

Two great features exist for handling edge cases in form submissions, `onSubmitInvalid` and `onSubmitMeta`/`submit(someMetaObj)`. However, when an invalid form is submitted with a `submitMeta` value, that value is not included in the arguments passed to `onSubmitInvalid` nor is it part of the TypeScript definitions for `onSubmitInvalid`.

This creates the problem where `onSubmitInvalid` is less capable in handling submissions than `onSubmit` which they should have parity between each other.

### Your minimal, reproducible example

https://github.com/TanStack/form/blob/ecfd47f3e8f65324f11d4c10355432f8a5f29392/packages/form-core/src/FormApi.ts#L327

### Steps to reproduce

Reproduction not necessary as the code and types are pretty clear.

* `FormOptions#onSubmitInvalid` type (does not include `TSubmitMeta` type)
   https://github.com/TanStack/form/blob/ecfd47f3e8f65324f11d4c10355432f8a5f29392/packages/form-core/src/FormApi.ts#L426-L443
* `FormOptions#onSubmit` type (_does_ include `TSubmitMeta` type)
   https://github.com/TanStack/form/blob/ecfd47f3e8f65324f11d4c10355432f8a5f29392/packages/form-core/src/FormApi.ts#L407-L425
* `onSubmitInvalid` call signature when a field is invalid (does not include `meta`)
   https://github.com/TanStack/form/blob/ecfd47f3e8f65324f11d4c10355432f8a5f29392/packages/form-core/src/FormApi.ts#L1785-L1788 
* `onSubmitInvalid` call signature when submission is invalid (does not include `meta`)
   https://github.com/TanStack/form/blob/ecfd47f3e8f65324f11d4c10355432f8a5f29392/packages/form-core/src/FormApi.ts#L1797-L1800 
* `onSubmit` call signature when form is valid (_does_ include `meta`)
   https://github.com/TanStack/form/blob/ecfd47f3e8f65324f11d4c10355432f8a5f29392/packages/form-core/src/FormApi.ts#L1819-L1823

### Expected behavior

`onSubmitInvalid` should receive the same arguments as `onSubmit`

### How often does this bug happen?

Every time

### Screenshots or Videos

_No response_

### Platform

macOS 15.5 (24F74)
Version 138.0.7204.169 (Official Build) (arm64)

### TanStack Form adapter

react-form

### TanStack Form version

v1.15.1

### TypeScript version

_No response_

### Additional context

_No response_


Discussion:

Patch:
diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts
index 1dfb98e97..73a08c020 100644
--- a/packages/form-core/src/FormApi.ts
+++ b/packages/form-core/src/FormApi.ts
@@ -314,6 +314,7 @@ export interface FormListeners<
       TOnServer,
       TSubmitMeta
     >
+    meta: TSubmitMeta
   }) => void
 }
 
@@ -440,6 +441,7 @@ export interface FormOptions<
       TOnServer,
       TSubmitMeta
     >
+    meta: TSubmitMeta
   }) => void
   transform?: FormTransform<
     NoInfer<TFormData>,
@@ -1772,6 +1774,9 @@ export class FormApi<
 
     if (!this.state.canSubmit) return
 
+    const submitMetaArg =
+      submitMeta ?? (this.options.onSubmitMeta as TSubmitMeta)
+
     this.baseStore.setState((d) => ({ ...d, isSubmitting: true }))
 
     const done = () => {
@@ -1785,6 +1790,7 @@ export class FormApi<
       this.options.onSubmitInvalid?.({
         value: this.state.values,
         formApi: this,
+        meta: submitMetaArg,
       })
       return
     }
@@ -1797,6 +1803,7 @@ export class FormApi<
       this.options.onSubmitInvalid?.({
         value: this.state.values,
         formApi: this,
+        meta: submitMetaArg,
       })
       return
     }
@@ -1812,15 +1819,15 @@ export class FormApi<
       )
     })
 
-    this.options.listeners?.onSubmit?.({ formApi: this })
+    this.options.listeners?.onSubmit?.({ formApi: this, meta: submitMetaArg })
 
     try {
       // Run the submit code
       await this.options.onSubmit?.({
         value: this.state.values,
         formApi: this,
-        meta: submitMeta ?? this.options.onSubmitMeta,
-      } as any)
+        meta: submitMetaArg,
+      })
 
       batch(() => {
         this.baseStore.setState((prev) => ({


Test patch:
diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts
index ebeb17124..fbab55e5d 100644
--- a/packages/form-core/tests/FormApi.spec.ts
+++ b/packages/form-core/tests/FormApi.spec.ts
@@ -3121,6 +3121,30 @@ describe('form api', () => {
     await form.handleSubmit({ dinosaur: 'Stegosaurus' })
   })
 
+  it('should pass the handleSubmit meta data to onSubmitInvalid', async () => {
+    const form = new FormApi({
+      onSubmitMeta: {} as { dinosaur: string },
+      onSubmitInvalid: async ({ meta }) => {
+        expect(meta.dinosaur).toEqual('Stegosaurus')
+      },
+    })
+
+    await form.handleSubmit({ dinosaur: 'Stegosaurus' })
+  })
+
+  it('should pass the handleSubmit meta data to the onSubmit listener', async () => {
+    const form = new FormApi({
+      onSubmitMeta: {} as { dinosaur: string },
+      listeners: {
+        onSubmit: ({ meta }) => {
+          expect(meta.dinosaur).toEqual('Stegosaurus')
+        },
+      },
+    })
+
+    await form.handleSubmit({ dinosaur: 'Stegosaurus' })
+  })
+
   it('should pass the handleSubmit default meta data to onSubmit', async () => {
     const form = new FormApi({
       onSubmitMeta: { dinosaur: 'Frank' } as { dinosaur: string },
@@ -3132,6 +3156,30 @@ describe('form api', () => {
     await form.handleSubmit()
   })
 
+  it('should pass the handleSubmit default meta data to onSubmitInvalid', async () => {
+    const form = new FormApi({
+      onSubmitMeta: { dinosaur: 'Frank' } as { dinosaur: string },
+      onSubmitInvalid: async ({ meta }) => {
+        expect(meta.dinosaur).toEqual('Frank')
+      },
+    })
+
+    await form.handleSubmit()
+  })
+
+  it('should pass the handleSubmit default meta data to the onSubmit listener', async () => {
+    const form = new FormApi({
+      onSubmitMeta: { dinosaur: 'Frank' } as { dinosaur: string },
+      listeners: {
+        onSubmit: ({ meta }) => {
+          expect(meta.dinosaur).toEqual('Frank')
+        },
+      },
+    })
+
+    await form.handleSubmit()
+  })
+
   it('should read and update union objects', async () => {
     const form = new FormApi({
       defaultValues: {