Handling save errors in backend and API
Overview
When server-side code persists changes through IUnitOfWork, Neos can return rich error information instead of just a failure message.
This article explains:
- how to retrieve save errors in backend code
- which information is available from the
UnitOfWorkin business code - how generated entity view APIs expose these errors in HTTP responses
Backend: retrieving errors from the UnitOfWork
If you want to inspect errors programmatically, prefer a save mode that returns a failed Result instead of throwing immediately.
The most practical option is:
Result saveResult = await _unitOfWork.TrySaveAsync(cancellationToken);
if (saveResult.IsFailed)
{
foreach (IError error in saveResult.Errors)
{
_logger.LogError("{ErrorType}: {Message}", error.GetType().Name, error.Message);
}
}
You can also use:
Result saveResult = await _unitOfWork.SaveAsync(SaveMode.NeverThrow, cancellationToken);
These two calls have the same behavior.
Important
After a failed SaveAsync(), or after ValidateOnly / Simulate, the current scope must not be reused for another save attempt. Create a new scope before retrying. See Unit of work pattern.
What you get after a failed save
Use:
Result.IsFailedIError.Message- IEntityViewAssociatedError when you need associated tracked items
Result saveResult = await _unitOfWork.TrySaveAsync(cancellationToken);
if (saveResult.IsFailed)
{
foreach (IError error in saveResult.Errors)
{
if (error is IEntityViewAssociatedError associatedError)
{
foreach (IEntityView item in associatedError.Items)
{
_logger.LogWarning("Error on tracked item {EntityViewType}: {Message}", item.GetType().Name, error.Message);
}
}
}
}
After a failed save, the useful information is:
- the global success or failure of the save through
Result - the human-readable message through
IError.Message - the associated entity views through
IEntityViewAssociatedError.Items
If you need a business-friendly description including a source or key, derive it from the associated IEntityView items and the repositories available in your code.
This is enough to implement practical server-side handling such as:
- import summaries
- per-item logging
- user-facing result messages returned by server methods
- correlation between a failing save and the tracked items involved in the operation
API format returned by generated entity view endpoints
Generated entity view controllers convert failed save results into HTTP 400 Bad Request responses using ApiError and ApiInnerError.
At the top level, the response follows a ProblemDetails-style shape and adds Neos-specific fields.
Typical top-level fields are:
typetitlestatusdetailwhen applicableinstancewhen applicabletraceIdtechnicalerrors
Validation error response example
{
"type": "https://doc.todo.com/errors/entity-view-validation-failed",
"title": "One or more entity view validations failed.",
"status": 400,
"traceId": "00-7f2d5c5d8f5c774287f6f8cfd18cdbf0-6c7c5e4d9db7f1a6-00",
"technical": false,
"errors": [
{
"type": "https://doc.todo.com/errors/entity-view-validation-failed",
"title": "One or more entity view validations failed.",
"detail": "The order date must not be later than today's date.",
"instance": "/entity-view-validation-failed",
"technical": false,
"extensions": {
"entityViewName": "AKOrderByNaturalKeyView",
"entityName": "AKOrder",
"keyProperties": [
{
"propertyName": "Number",
"value": "ORD001"
}
],
"path": "items[0]"
}
}
]
}
Information available in validation responses
For validation failures, each inner error may expose the following additional data in errors[].extensions:
entityViewName: name of the entity viewentityName: underlying entity name when availablekeyProperties: key property/value pairspath: path of the failing item relative to the request payload when it can be resolved
keyProperties is flattened for nested keys. If a key property is itself a referenced entity or entity view, Neos expands it using dotted property names.
Example:
[
{ "propertyName": "Order.Number", "value": "ORD001" },
{ "propertyName": "Product.Code", "value": "PROD001" }
]
Validation response without entity view path information
In some validation failure cases, the API returns entity-level metadata only:
entityNamekeyProperties
In that case entityViewName and path are not available.
Event error response
For event rule failures, generated entity view APIs always return the business-visible error message in detail.
In development environment only, they may also add a technical exception entry.
Example outside development environment:
{
"type": "https://doc.todo.com/errors/entity-view-event-failed",
"title": "One or more entity view events failed.",
"status": 400,
"traceId": "00-7f2d5c5d8f5c774287f6f8cfd18cdbf0-6c7c5e4d9db7f1a6-00",
"technical": false,
"errors": [
{
"type": "https://doc.todo.com/errors/entity-view-event-failed",
"title": "One or more entity view events failed.",
"detail": "A business rule prevented the save.",
"instance": "/entity-view-event-failed",
"technical": false
}
]
}
Example in development environment:
{
"type": "https://doc.todo.com/errors/entity-view-event-failed",
"title": "One or more entity view events failed.",
"status": 400,
"traceId": "00-7f2d5c5d8f5c774287f6f8cfd18cdbf0-6c7c5e4d9db7f1a6-00",
"technical": false,
"errors": [
{
"type": "https://doc.todo.com/errors/entity-view-event-failed",
"title": "One or more entity view events failed.",
"detail": "A business rule prevented the save.",
"instance": "/entity-view-event-failed",
"technical": false,
"extensions": {
"exception": "GroupeIsa.Neos.Domain.Exceptions.BusinessException: A business rule prevented the save."
}
}
]
}
For these event failures:
They expose:
detail: the message intended to explain why the save was rejectedexceptiononly in development environment
Outside development environment, they do not expose:
- the .NET exception object
- the stack trace
- internal exception metadata
detail versus exception
detail and exception do not serve the same purpose.
detailis part of the functional API contract. It is meant to be read by API consumers and client applications to understand the failure.exceptionis technical diagnostic data. It must not be used as a functional contract.
For save errors returned as failed Result values by generated entity view endpoints:
- validation failures use
detailand may add business metadata such asentityViewName,entityName,keyProperties, orpath - event failures use
detailand may addexceptionin development environment only - outside development environment, the raw exception is not returned
An exception field may still appear in other API error paths, for example:
- a
BusinessExceptionhandled by the generic API error controller in development environment - an unhandled exception handled by the generic API error controller in development environment
In every case, exception is development-oriented troubleshooting data and is only available in development environment.
Special top-level API metadata
Some generated POST/PUT endpoints may add extra top-level metadata.
Current example:
saveSuccess:trueorfalse
This flag is used when the database save succeeded but the API failed afterward while trying to reload the saved item. In that case, the save is already committed even though the final HTTP response is an error.
Recommended patterns
For backend code
- Use
TrySaveAsync()when you need to inspect errors programmatically. - Use
IEntityViewAssociatedErrorwhen you need to know which tracked items are associated with an error. - Treat a failed save result as terminal for the current scope and retry in a new scope only.
For API consumers
- Read
errors[]rather than relying only on the top-leveltitle. - Use
errors[].extensions.keyPropertiesto identify the failing business object. - Use
errors[].extensions.pathto map the error back to the submitted payload when available. - Use
errors[].detailas the stable per-error explanation. - Do not treat
exceptionas a stable functional contract; it is technical diagnostic data that is only available in development environment.