Table of Contents

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 UnitOfWork in 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 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:

  • type
  • title
  • status
  • detail when applicable
  • instance when applicable
  • traceId
  • technical
  • errors

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 view
  • entityName: underlying entity name when available
  • keyProperties: key property/value pairs
  • path: 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:

  • entityName
  • keyProperties

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 rejected
  • exception only 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.

  • detail is part of the functional API contract. It is meant to be read by API consumers and client applications to understand the failure.
  • exception is 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 detail and may add business metadata such as entityViewName, entityName, keyProperties, or path
  • event failures use detail and may add exception in 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 BusinessException handled 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: true or false

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.

For backend code

  • Use TrySaveAsync() when you need to inspect errors programmatically.
  • Use IEntityViewAssociatedError when 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-level title.
  • Use errors[].extensions.keyProperties to identify the failing business object.
  • Use errors[].extensions.path to map the error back to the submitted payload when available.
  • Use errors[].detail as the stable per-error explanation.
  • Do not treat exception as a stable functional contract; it is technical diagnostic data that is only available in development environment.