How Formily Handles Validation Errors: From Core Logic to UI Display

Formily processes validation errors by executing rules through the core Field model, converting results into structured Feedback objects, and exposing reactive getters like selfErrors that UI components consume to render messages.

Formily is Alibaba's high-performance form solution designed for enterprise-grade applications. Its validation system decouples rule execution from presentation, using a reactive feedback mechanism that bridges the core data layer with framework-specific UI components. Understanding this architecture allows developers to debug complex validation scenarios and implement custom error displays effectively.

The Core Validation Architecture

Formily's validation flow revolves around three interconnected concepts that separate concerns between data validation and UI rendering:

  • Validator – A set of rules (required, max, custom functions) defined on a field's validator property and parsed by @formily/validator.
  • Feedback – A unified data structure containing type, messages, code, and triggerType that stores validation results.
  • UI Bindings – Components like FormItem read the field's selfErrors, selfWarnings, selfSuccesses, and validateStatus to render visual indicators.

When a field value changes, Formily automatically runs validation, converts the results into feedback objects, stores them on the field instance, and UI components reactively display the updated messages.

Validation Execution Flow

Triggering Validation

Validation can be triggered on multiple lifecycles including onInput, onFocus, onBlur, or explicit calls to .validate(). In packages/core/src/models/Field.ts, the validate method initiates this process:

// packages/core/src/models/Field.ts
validate = (triggerType?: ValidatorTriggerType) => {
  return batchValidate(this, `${this.address}.**`, triggerType)
}

This method delegates to batchValidate in packages/core/src/shared/internals.ts, which ultimately invokes validateToFeedbacks to process the field's specific rules.

Rule Execution in @formily/validator

The validateToFeedbacks function calls the library-level validate function from the validator package to execute rules against the current value:

// packages/core/src/shared/internals.ts
const results = await validate(
  field.value,
  field.validator,
  {
    triggerType,
    validateFirst: field.props.validateFirst ?? field.form.props.validateFirst,
    context: { field, form: field.form },
  }
)

The validate function in packages/validator/src/validator.ts iterates over each parsed rule and returns a results object structured as { error: ['msg1'], warning: [], success: [] }.

Transforming Results to Feedback

After validation completes, each result type is converted into a Feedback object and stored on the field:

// packages/core/src/shared/internals.ts
each(results, (messages, type: FieldFeedbackTypes) => {
  field.setFeedback({
    triggerType,
    type,
    code: pascalCase(`validate-${type}`),
    messages,
  })
})

Persisting Feedback State

The field.setFeedback method forwards to updateFeedback in the internals module:

// packages/core/src/models/Field.ts
setFeedback = (feedback?: IFieldFeedback) => {
  updateFeedback(this, feedback)
}

The updateFeedback function merges new feedback with existing entries, removes empty results, and maintains the feedbacks array on the field instance.

Accessing Validation State

Storage Structure

Each Field instance maintains a feedbacks: IFieldFeedback[] array that serves as an observable store for all validation messages.

Query Helpers

Formily provides utility functions in packages/core/src/shared/internals.ts to filter and retrieve feedback:

  • queryFeedbacks(field, search?) – Filters feedback by type, address, or other criteria.
  • queryFeedbackMessages(field, {type: 'error'}) – Returns only message strings for the specified type.

Computed Getters

The Field model exposes computed getters that UI components consume:

// packages/core/src/models/Field.ts
get selfErrors(): FeedbackMessage {
  return queryFeedbackMessages(this, { type: 'error' })
}
// Similarly: selfWarnings, selfSuccesses, validateStatus

These getters automatically update when the underlying feedbacks array changes, providing reactive access to the current validation state.

Rendering Errors in UI Components

UI components connect to the field's validation state through the connect and mapProps utilities. The FormItem component in packages/element/src/form-item/index.ts demonstrates this pattern:

// packages/element/src/form-item/index.ts (excerpt)
const Item = connect(
  FormBaseItem,
  mapProps(
    { validateStatus: true, title: 'label', required: true },
    (props, field) => {
      if (isVoidField(field) || !field) return props
      const takeMessage = () => {
        if (field.validating) return
        if (props.feedbackText) return props.feedbackText
        if (field.selfErrors.length) return field.selfErrors
        if (field.selfWarnings.length) return field.selfWarnings
        if (field.selfSuccesses.length) return field.selfSuccesses
      }
      const errorMessages = takeMessage()
      return {
        feedbackText: Array.isArray(errorMessages)
          ? errorMessages.join(', ')
          : errorMessages,
        extra: props.extra || field.description,
      }
    }
  )
)

The component maps field.selfErrors to the feedbackText prop and derives validateStatus from field.validateStatus. The template applies CSS classes like ${prefixCls}-error based on these values, automatically rendering red borders, error icons, and message text when validation fails.

Custom Validation Implementation

Here is a complete example demonstrating how Formily handles custom async validation:

import { createForm } from '@formily/core'
import { Form, Field } from '@formily/react'

const form = createForm({
  values: { username: '' },
})

function MyForm() {
  return (
    <Form form={form}>
      <Field
        name="username"
        decorator={[FormItem, { label: 'Username' }]}
        component={['Input']}
        validator={[
          { required: true, message: 'Username is required' },
          { minLength: 4, message: 'At least 4 characters' },
          async (value) => {
            const exists = await checkUserExists(value)
            return exists ? 'User already taken' : null
          },
        ]}
      />
    </Form>
  )
}

When the user types, onInput triggers validation, which runs the three rules sequentially. Errors are stored in field.selfErrors, and FormItem automatically displays them without additional configuration.

Summary

  • Validation triggers execute through the Field model's validate method, which delegates to batchValidate and validateToFeedbacks.
  • Rule execution occurs in @formily/validator, returning typed results that the core converts into Feedback objects.
  • State persistence happens via updateFeedback, which manages the feedbacks array on each Field instance.
  • Reactive access is provided through getters like selfErrors and validateStatus.
  • UI display is handled by components like FormItem that map field state to visual elements using connect and mapProps.

Frequently Asked Questions

How does Formily trigger validation automatically?

Formily triggers validation automatically through lifecycle hooks on field interactions. When a user triggers onInput, onFocus, or onBlur, the Field model invokes validateSelf, which runs the validation pipeline and updates the feedback state reactively.

What is the difference between selfErrors and feedbacks?

The feedbacks array is the raw storage containing all validation results with metadata like triggerType and code. The selfErrors getter is a computed property that filters this array to return only error-type messages, providing a convenient API for UI components to consume.

How can I customize when validation errors appear?

Control validation timing through the triggerType parameter in validator rules or field props. You can set validation to run on specific events like onBlur rather than onInput, or use the validateFirst option to stop after the first error. These options are passed through the context to validateToFeedbacks in packages/core/src/shared/internals.ts.

Where does Formily store validation error messages?

Formily stores validation error messages in the feedbacks observable array on each Field instance, defined in packages/core/src/models/Field.ts. The messages are inserted via setFeedback and updateFeedback, then accessed through computed getters like selfErrors for display in UI components.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →