|
| 1 | +--- |
| 2 | +id: custom-errors |
| 3 | +title: Custom Errors |
| 4 | +--- |
| 5 | + |
| 6 | +TanStack Form provides complete flexibility in the types of error values you can return from validators. String errors are the most common and easy to work with, but the library allows you to return any type of value from your validators. |
| 7 | + |
| 8 | +As a general rule, any truthy value is considered as an error and will mark the form or field as invalid, while falsy values (`false`, `undefined`, `null`, etc..) mean there is no error, the form or field is valid. |
| 9 | + |
| 10 | +## Return String Values from Forms |
| 11 | + |
| 12 | +```tsx |
| 13 | +<form.Field |
| 14 | + name="username" |
| 15 | + validators={{ |
| 16 | + onChange: ({ value }) => |
| 17 | + value.length < 3 ? 'Username must be at least 3 characters' : undefined, |
| 18 | + }} |
| 19 | +/> |
| 20 | +``` |
| 21 | + |
| 22 | +For form-level validation affecting multiple fields: |
| 23 | + |
| 24 | +```tsx |
| 25 | +const form = useForm({ |
| 26 | + defaultValues: { |
| 27 | + username: '', |
| 28 | + email: '', |
| 29 | + }, |
| 30 | + validators: { |
| 31 | + onChange: ({ value }) => { |
| 32 | + return { |
| 33 | + fields: { |
| 34 | + username: |
| 35 | + value.username.length < 3 ? 'Username too short' : undefined, |
| 36 | + email: !value.email.includes('@') ? 'Invalid email' : undefined, |
| 37 | + }, |
| 38 | + } |
| 39 | + }, |
| 40 | + }, |
| 41 | +}) |
| 42 | +``` |
| 43 | + |
| 44 | +String errors are the most common type and are easily displayed in your UI: |
| 45 | + |
| 46 | +```tsx |
| 47 | +{ |
| 48 | + field.state.meta.errors.map((error, i) => ( |
| 49 | + <div key={i} className="error"> |
| 50 | + {error} |
| 51 | + </div> |
| 52 | + )) |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +### Numbers |
| 57 | + |
| 58 | +Useful for representing quantities, thresholds, or magnitudes: |
| 59 | + |
| 60 | +```tsx |
| 61 | +<form.Field |
| 62 | + name="age" |
| 63 | + validators={{ |
| 64 | + onChange: ({ value }) => (value < 18 ? 18 - value : undefined), |
| 65 | + }} |
| 66 | +/> |
| 67 | +``` |
| 68 | + |
| 69 | +Display in UI: |
| 70 | + |
| 71 | +```tsx |
| 72 | +{ |
| 73 | + /* TypeScript knows the error is a number based on your validator */ |
| 74 | +} |
| 75 | +;<div className="error"> |
| 76 | + You need {field.state.meta.errors[0]} more years to be eligible |
| 77 | +</div> |
| 78 | +``` |
| 79 | + |
| 80 | +### Booleans |
| 81 | + |
| 82 | +Simple flags to indicate error state: |
| 83 | + |
| 84 | +```tsx |
| 85 | +<form.Field |
| 86 | + name="accepted" |
| 87 | + validators={{ |
| 88 | + onChange: ({ value }) => (!value ? true : undefined), |
| 89 | + }} |
| 90 | +/> |
| 91 | +``` |
| 92 | + |
| 93 | +Display in UI: |
| 94 | + |
| 95 | +```tsx |
| 96 | +{ |
| 97 | + field.state.meta.errors[0] === true && ( |
| 98 | + <div className="error">You must accept the terms</div> |
| 99 | + ) |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +### Objects |
| 104 | + |
| 105 | +Rich error objects with multiple properties: |
| 106 | + |
| 107 | +```tsx |
| 108 | +<form.Field |
| 109 | + name="email" |
| 110 | + validators={{ |
| 111 | + onChange: ({ value }) => { |
| 112 | + if (!value.includes('@')) { |
| 113 | + return { |
| 114 | + message: 'Invalid email format', |
| 115 | + severity: 'error', |
| 116 | + code: 1001, |
| 117 | + } |
| 118 | + } |
| 119 | + return undefined |
| 120 | + }, |
| 121 | + }} |
| 122 | +/> |
| 123 | +``` |
| 124 | + |
| 125 | +Display in UI: |
| 126 | + |
| 127 | +```tsx |
| 128 | +{ |
| 129 | + typeof field.state.meta.errors[0] === 'object' && ( |
| 130 | + <div className={`error ${field.state.meta.errors[0].severity}`}> |
| 131 | + {field.state.meta.errors[0].message} |
| 132 | + <small> (Code: {field.state.meta.errors[0].code})</small> |
| 133 | + </div> |
| 134 | + ) |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +in the example above it depends on the event error you want to display. |
| 139 | + |
| 140 | +### Arrays |
| 141 | + |
| 142 | +Multiple error messages for a single field: |
| 143 | + |
| 144 | +```tsx |
| 145 | +<form.Field |
| 146 | + name="password" |
| 147 | + validators={{ |
| 148 | + onChange: ({ value }) => { |
| 149 | + const errors = [] |
| 150 | + if (value.length < 8) errors.push('Password too short') |
| 151 | + if (!/[A-Z]/.test(value)) errors.push('Missing uppercase letter') |
| 152 | + if (!/[0-9]/.test(value)) errors.push('Missing number') |
| 153 | + |
| 154 | + return errors.length ? errors : undefined |
| 155 | + }, |
| 156 | + }} |
| 157 | +/> |
| 158 | +``` |
| 159 | + |
| 160 | +Display in UI: |
| 161 | + |
| 162 | +```tsx |
| 163 | +{ |
| 164 | + Array.isArray(field.state.meta.errors) && ( |
| 165 | + <ul className="error-list"> |
| 166 | + {field.state.meta.errors.map((err, i) => ( |
| 167 | + <li key={i}>{err}</li> |
| 168 | + ))} |
| 169 | + </ul> |
| 170 | + ) |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +## The `disableErrorFlat` Prop on Fields |
| 175 | + |
| 176 | +By default, TanStack Form flattens errors from all validation sources (onChange, onBlur, onSubmit) into a single `errors` array. The `disableErrorFlat` prop preserves the error sources: |
| 177 | + |
| 178 | +```tsx |
| 179 | +<form.Field |
| 180 | + name="email" |
| 181 | + disableErrorFlat |
| 182 | + validators={{ |
| 183 | + onChange: ({ value }) => |
| 184 | + !value.includes('@') ? 'Invalid email format' : undefined, |
| 185 | + onBlur: ({ value }) => |
| 186 | + !value.endsWith('.com') ? 'Only .com domains allowed' : undefined, |
| 187 | + onSubmit: ({ value }) => (value.length < 5 ? 'Email too short' : undefined), |
| 188 | + }} |
| 189 | +/> |
| 190 | +``` |
| 191 | + |
| 192 | +Without `disableErrorFlat`, all errors would be combined into `field.state.meta.errors`. With it, you can access errors by their source: |
| 193 | + |
| 194 | +```tsx |
| 195 | +{ |
| 196 | + field.state.meta.errorMap.onChange && ( |
| 197 | + <div className="real-time-error">{field.state.meta.errorMap.onChange}</div> |
| 198 | + ) |
| 199 | +} |
| 200 | + |
| 201 | +{ |
| 202 | + field.state.meta.errorMap.onBlur && ( |
| 203 | + <div className="blur-feedback">{field.state.meta.errorMap.onBlur}</div> |
| 204 | + ) |
| 205 | +} |
| 206 | + |
| 207 | +{ |
| 208 | + field.state.meta.errorMap.onSubmit && ( |
| 209 | + <div className="submit-error">{field.state.meta.errorMap.onSubmit}</div> |
| 210 | + ) |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +This is useful for: |
| 215 | + |
| 216 | +- Displaying different types of errors with different UI treatments |
| 217 | +- Prioritizing errors (e.g., showing submission errors more prominently) |
| 218 | +- Implementing progressive disclosure of errors |
| 219 | + |
| 220 | +## Type Safety of `errors` and `errorMap` |
| 221 | + |
| 222 | +TanStack Form provides strong type safety for error handling. Each key in the `errorMap` has exactly the type returned by its corresponding validator, while the `errors` array contains a union type of all the possible error values from all validators: |
| 223 | + |
| 224 | +```tsx |
| 225 | +<form.Field |
| 226 | + name="password" |
| 227 | + validators={{ |
| 228 | + onChange: ({ value }) => { |
| 229 | + // This returns a string or undefined |
| 230 | + return value.length < 8 ? 'Too short' : undefined |
| 231 | + }, |
| 232 | + onBlur: ({ value }) => { |
| 233 | + // This returns an object or undefined |
| 234 | + if (!/[A-Z]/.test(value)) { |
| 235 | + return { message: 'Missing uppercase', level: 'warning' } |
| 236 | + } |
| 237 | + return undefined |
| 238 | + }, |
| 239 | + }} |
| 240 | + children={(field) => { |
| 241 | + // TypeScript knows that errors[0] can be string | { message: string, level: string } | undefined |
| 242 | + const error = field.state.meta.errors[0] |
| 243 | + |
| 244 | + // Type-safe error handling |
| 245 | + if (typeof error === 'string') { |
| 246 | + return <div className="string-error">{error}</div> |
| 247 | + } else if (error && typeof error === 'object') { |
| 248 | + return <div className={error.level}>{error.message}</div> |
| 249 | + } |
| 250 | + |
| 251 | + return null |
| 252 | + }} |
| 253 | +/> |
| 254 | +``` |
| 255 | + |
| 256 | +The `errorMap` property is also fully typed, matching the return types of your validation functions: |
| 257 | + |
| 258 | +```tsx |
| 259 | +// With disableErrorFlat |
| 260 | +<form.Field |
| 261 | + name="email" |
| 262 | + disableErrorFlat |
| 263 | + validators={{ |
| 264 | + onChange: ({ value }): string | undefined => |
| 265 | + !value.includes("@") ? "Invalid email" : undefined, |
| 266 | + onBlur: ({ value }): { code: number, message: string } | undefined => |
| 267 | + !value.endsWith(".com") ? { code: 100, message: "Wrong domain" } : undefined |
| 268 | + }} |
| 269 | + children={(field) => { |
| 270 | + // TypeScript knows the exact type of each error source |
| 271 | + const onChangeError: string | undefined = field.state.meta.errorMap.onChange; |
| 272 | + const onBlurError: { code: number, message: string } | undefined = field.state.meta.errorMap.onBlur; |
| 273 | + |
| 274 | + return (/* ... */); |
| 275 | + }} |
| 276 | +/> |
| 277 | +``` |
| 278 | + |
| 279 | +This type safety helps catch errors at compile time instead of runtime, making your code more reliable and maintainable. |
0 commit comments