Error Handling
When validation fails, you want to know what went wrong and where. safeParse() hands back a SchemaResult holding either the validated value or a SchemaError, so you can inspect failures without try/catch.
The SchemaResult object
safeParse() returns a SchemaResult<T>. Its value is nullable — a nullable schema can succeed with null.
result.isOk:trueif validation succeeded.result.isFail:trueif validation failed.result.getOrThrow(): Returns the validated value, or throwsAckExceptionon failure.result.getOrNull(): Returns the validated value, ornullon failure.result.getError(): Returns theSchemaErroron failure, or throwsStateErrorif called on a success.result.getOrElse(() => defaultValue): Returns the validated value on success, or calls the fallback on failure.result.match(onOk: (value) => ..., onFail: (error) => ...): Collapses both cases into a single value.result.map((value) => ...): Transforms the success value and leaves failures untouched.result.ifOk((value) { ... })/result.ifFail((error) { ... }): Runs a side effect for just one case.
import 'package:ack/ack.dart';
final schema = Ack.string().minLength(5);
final result = schema.safeParse('abc');
if (result.isFail) {
final error = result.getError();
print('Validation failed: $error');
// Try to get data (will throw)
try {
result.getOrThrow();
} catch (e) {
print('Caught exception: $e');
}
// Get a default value
final dataOrDefault = result.getOrElse(() => 'default_string');
print('Data or default: $dataOrDefault'); // Output: default_string
}Understanding SchemaError
SchemaError contains details about the validation failure.
error.name: The context name at the failure point (for example, a field name or thedebugNamepassed tosafeParse).error.value: The value that failed validation.error.schema: The schema being validated against.error.path: The RFC 6901 JSON Pointer path to the failure (for example,#/address/city).
final userSchema = Ack.object({
'name': Ack.string(),
'age': Ack.integer().min(18),
'address': Ack.object({
'city': Ack.string()
})
});
final invalidData = {
'name': 'Test',
'age': 15, // Fails min(18)
'address': {
'city': 123 // Fails Ack.string
}
};
final result = userSchema.safeParse(invalidData);
if (result.isFail) {
final error = result.getError();
print('Error Name: ${error.name}'); // Example: 'address'
print('Error Path: ${error.path}'); // Example: '#/address/city'
print('Failed Value: ${error.value}'); // Example: the invalid data
print('Full Error: $error'); // Complete error details
// Note: The exact error structure (especially for nested errors) can vary.
// Sometimes the top-level error might be SchemaNestedError,
// and you might need to inspect its nested errors.
}Error types
Each SchemaError subtype carries specific information about why validation failed.
TypeMismatchError
Raised when the input type doesn’t match the expected schema type.
final schema = Ack.string();
final result = schema.safeParse(42); // Number instead of string
if (result.isFail) {
final error = result.getError() as TypeMismatchError;
print('Expected: ${error.expectedType}'); // "string"
print('Actual: ${error.actualType}'); // "integer"
print('Error: ${error.message}'); // "Expected string, got integer"
}SchemaConstraintsError
Raised when the value violates one or more validation constraints (such as minLength, min, or email).
final schema = Ack.string().minLength(5).email();
final result = schema.safeParse('abc');
if (result.isFail) {
final error = result.getError() as SchemaConstraintsError;
print('Constraint violations: ${error.constraints.length}');
// Access individual failed constraints
for (final constraintError in error.constraints) {
print('- Constraint: ${constraintError.constraint}');
print('- Message: ${constraintError.message}');
}
}SchemaNestedError
Raised when validation fails within nested objects or lists. Contains a list of child errors.
final schema = Ack.object({
'name': Ack.string().minLength(2),
'age': Ack.integer().min(0),
});
final result = schema.safeParse({
'name': 'J', // Too short
'age': -5, // Negative
});
if (result.isFail) {
final error = result.getError() as SchemaNestedError;
print('Nested errors: ${error.errors.length}');
// Recursively inspect nested errors
for (final nestedError in error.errors) {
print('- Field: ${nestedError.name}');
print('- Error: ${nestedError.message}');
}
}SchemaValidationError
Raised when custom validation logic added via .refine() fails.
final schema = Ack.integer().refine(
(value) => value % 2 == 0,
message: 'Must be an even number',
);
final result = schema.safeParse(3);
if (result.isFail) {
final error = result.getError() as SchemaValidationError;
print('Validation error: ${error.message}'); // "Must be an even number"
}SchemaTransformError
Raised when a .transform() callback throws an exception.
Callbacks receive the non-null validated runtime value; null handling belongs to the surrounding nullable/withDefault configuration. SchemaTransformError fires when the callback itself throws — for example, when the input passes validation but the conversion fails:
final schema = Ack.string().transform((value) {
final parsed = int.tryParse(value);
if (parsed == null) throw FormatException('Not numeric: $value');
return parsed;
});
final result = schema.safeParse('not-a-number');
if (result.isFail) {
final error = result.getError() as SchemaTransformError;
print('Transform error: ${error.message}');
print('Cause: ${error.cause}'); // Original exception
}SchemaEncodeError
Raised when encode() or safeEncode() can’t turn a runtime value back into its boundary form — for example, encoding a one-way transform, or a value that breaks a codec’s invariant (such as a non-UTC DateTime for Ack.datetime()). Its kind field says which case occurred.
Working with error types
Use pattern matching to handle specific error types:
final result = schema.safeParse(data);
if (result.isFail) {
final error = result.getError();
switch (error) {
case TypeMismatchError():
print('Wrong type: expected ${error.expectedType}');
case SchemaConstraintsError():
print('Failed ${error.constraints.length} constraints');
case SchemaNestedError():
print('Nested validation failed at ${error.errors.length} locations');
case SchemaValidationError():
print('Custom validation failed: ${error.message}');
case SchemaTransformError():
print('Transformation failed: ${error.cause}');
case SchemaEncodeError():
print('Could not encode value: ${error.message}');
default:
print('Validation failed: ${error.message}');
}
}Displaying errors in UI (Flutter example)
In a TextFormField validator, return the error string directly:
// Inside TextFormField validator
validator: (value) {
final result = someSchema.safeParse(value);
if (result.isFail) {
return result.getError().toString();
}
return null;
}See the Form Validation Guide for details.
Custom error messages
Built-in constraints ship with default messages. For a custom message, pass message: to .refine(), or attach a constraint with .constrain(..., message: ...) — see Custom Validation.
final schema = Ack.string().refine(
(value) => value.startsWith('ACK_'),
message: 'Value must start with ACK_',
);
final result = schema.safeParse('nope');
if (result.isFail) {
print(result.getError().message); // Value must start with ACK_
}Next steps
- Custom Validation: Create constraints with custom error messages
- Flutter Forms: Display validation errors in Flutter form widgets
- Validation Rules: All built-in validation constraints
- Schema Types: Schema types and their behavior
- Common Recipes: Practical error handling patterns