Skip to content

Basics

Validation and Error Display

Validations live on the model and run automatically on save() and update(). Every failure collects onto the record as an error you can read from the controller or render into a view. This page walks the built-in helpers, the errors API, and the two patterns for surfacing errors back to the user.

You’ll learn:

  • How to declare validations in config() and which helpers ship with the framework
  • When validations fire — save(), update(), create(), and the manual .valid() trigger
  • How to read errors with hasErrors(), errors(), errorsOn(), and how to add your own with addError()
  • Two ways to render errors in a view — the summary helper and per-field output
  • How to write custom validation methods and when to scope validations with when or condition

Validations are declared in the model’s config() method. Each helper registers a rule; the framework runs them all every time the record is saved, collects the failures as errors, and short-circuits the save if any fail.

component extends="Model" {
function config() {
validatesPresenceOf(properties="title,body");
validatesLengthOf(property="title", maximum=120);
validatesUniquenessOf(property="slug");
}
}

The plural properties="title,body" and singular property="title" arguments are aliases for the same underlying value — use whichever reads better for the field count. Rules registered via these helpers all accept a message argument for a custom failure message.

Every helper below is a thin wrapper over the same registration machinery. The property / properties argument accepts either a single name or a comma-separated list.

  • validatesPresenceOf(properties="name,email") — value must not be blank.
  • validatesLengthOf(property="title", maximum=120, minimum=3) — length bounds. Also accepts exactly and within (a two-value list like "3,120").
  • validatesFormatOf(property="email", type="email") — format check. Pass regEx="..." for a custom pattern, or type for one of the built-in types (creditcard, date, email, eurodate, guid, ssn, telephone, time, URL, USdate, UUID, variableName, zipcode, boolean).
  • validatesUniquenessOf(property="email", scope="organizationId") — runs a DB check; fails if another row already has the value. scope narrows the uniqueness check to rows matching one or more properties.
  • validatesInclusionOf(property="status", list="draft,published,archived") — value must appear in list.
  • validatesExclusionOf(property="subdomain", list="admin,www,api") — value must not appear in list. Useful for reserved words.
  • validatesNumericalityOf(property="quantity", greaterThan=0) — numeric checks. Also accepts onlyInteger, odd, even, greaterThanOrEqualTo, equalTo, lessThan, lessThanOrEqualTo.
  • validatesConfirmationOf(property="password") — matches password against passwordConfirmation. The confirmation field never gets saved to the database.

Wheels runs validations automatically on most write paths. You can also trigger them manually without touching the database.

  • save() and update() — validations run before the write. If any fail, the call returns false, errors are attached to the record, and the database is never touched.
  • create() — shortcut for new() + save(); validations run the same way.
  • .valid() — manually run validations on an unsaved record. Returns true / false and populates errors. Use this when you need to validate before displaying a form, or in background work where you control the save later.
  • save(validate=false) — skip validations entirely. Rare, and only safe when you know the data is already clean (seeds, data imports, migrations).

Both create and update callbacks fire at the right time: when="onCreate" rules only run for new records, when="onUpdate" rules only run when updating an existing row, when="onSave" (the default) runs in both cases.

When save() returns false, the record carries the failures with it. The controller’s job is to check and re-render:

component extends="Controller" {
function create() {
post = model("Post").new(params.post);
if (post.save()) {
redirectTo(route="post", key=post.id);
} else {
renderView(action="new");
}
}
}

On the record itself, four methods cover the common cases:

  • hasErrors() — boolean. Pass a property name (hasErrors("title")) to narrow to a single field.
  • errors() — array of error structs. Each struct has property and message.
  • errorsOn("title") — array of error structs for a single property. Returns an empty array when none.
  • addError(property="title", message="...") — attach an error manually. Useful from a beforeSave callback or a custom validation method.

Because save() already populates errors before returning false, there is nothing extra to do in the controller — just re-render the form and let the view read the record.

The fastest way to show every error is errorMessagesFor. Drop it above the form and Wheels renders a <ul class="error-messages"> with one <li> per error. When there are no errors, it returns an empty string.

<cfoutput>
##startFormTag(route="posts")##
##errorMessagesFor("post")##
##textField(objectName="post", property="title")##
##textArea(objectName="post", property="body")##
<button type="submit">Save</button>
##endFormTag()##
</cfoutput>

The argument is the view-scope variable name — "post" here matches posts in the new action. See Forms and Form Helpers for the wider form flow this slots into.

Object-bound form helpers can nest the first error for a property inside the field’s error wrapper. Pass includeErrorMessage=true on the helper, or opt in globally with set(includeFormErrorMessages=true) (the wheels new template does this). The message is prefixed with Error: and marked role="alert" so it is not color-only.

The short path is still errorMessageOn, which returns the first error for a property (empty string when none):

<cfoutput>
##textField(objectName="post", property="title")##
##errorMessageOn(objectName="post", property="title", wrapperElement="span", class="error")##
</cfoutput>

For full control over markup, loop errorsOn yourself:

<cfoutput>
<cfset titleErrors = post.errorsOn("title")>
<cfif ArrayLen(titleErrors) gt 0>
<span class="error">##titleErrors[1].message##</span>
</cfif>
</cfoutput>

When a built-in helper doesn’t cover the rule — cross-field checks, external lookups, conditional uniqueness — register a custom method. The method is private, returns nothing, and calls addError() whenever it decides the record is invalid.

component extends="Model" {
function config() {
validate(methods="mustBeUniqueAcrossTenants");
}
private function mustBeUniqueAcrossTenants() {
if (model("Post").exists(where="slug='##this.slug##' AND orgId=##this.orgId##")) {
addError(property="slug", message="Already in use in this organization");
}
}
}

Register one or more methods via validate(methods="method1,method2"). Use validateOnCreate or validateOnUpdate to limit the trigger. Inside the method, this is the record under validation — read any property off it and call addError (or addErrorToBase for record-wide errors that don’t belong to a specific field).

Every built-in helper (and validate itself) accepts when, condition, and unless. Use them to scope a rule to a specific lifecycle event or to a runtime state.

component extends="Model" {
function config() {
validatesPresenceOf(property="approverId", when="onUpdate");
validatesLengthOf(property="title", maximum=120, condition="this.status != 'draft'");
}
}
  • when — "onSave" (default, fires on create and update), "onCreate", or "onUpdate".
  • condition — an expression evaluated against the record. The rule runs only when it returns true.
  • unless — inverse of condition; the rule runs only when the expression returns false.

condition and unless are not evaluated as CFML. Wheels parses a small expression language. Many strings it can’t parse throw Wheels.InvalidValidationCondition when the record is validated, but some give a wrong answer without an error, so stay within the supported forms:

  • this.property or this.method() (named key='val' or positional 'val' arguments), optionally compared: this.status != 'draft', this.score >= 10.
  • A bare method call such as isNew() or !isNew().
  • Whitelisted functions: StructKeyExists, IsNull, IsNumeric, IsSimpleValue, IsStruct, IsArray, IsBoolean, IsDate, Len, ListFind, ListFindNoCase, ListContains, ListContainsNoCase, for example StructKeyExists(this, 'requestFor') or ListFind('24,32,63', this.countryId).
  • Comparisons using eq neq lt lte gt gte or == != < <= > >=, compound && / || (&& binds tighter), ! and parentheses.

Put this. on every property. A bare name is not read as the property: on its own (isActive) it evaluates to false, and on the left of a comparison (status != 'draft') the word itself is compared, so that rule always runs. On the right of a comparison, a bare word is a string literal (this.status neq draft compares with the text draft). Keep this. references on the left (this.frequency eq 'weekly', not 'weekly' eq this.frequency), and use &&, ||, ! and eq rather than and, or, not and is: not throws, and the other three can give a wrong answer without an error. For anything more complex, move the logic into a model method and use condition="this.needsCheck()".