---
title: SAIF.NetCore.ErrorHandler business rules
summary: Catalog of business rules enforced by this library.
status: draft
owner: Framework.NCore maintainers
last_reviewed: 2026-10-02
library: SAIF.NetCore.ErrorHandler
reference_type: package
---

# SAIF.NetCore.ErrorHandler business rules

This page catalogs every business rule this library enforces, identified by stable rule IDs referenced from operations and API reference pages.

## EH-BR-01 An error response is not written once the HTTP response has started

| Field | Value |
| --- | --- |
| **Statement** | An error response is not written once the HTTP response has started. A warning is logged and the original exception is re-thrown. |
| **Trigger** | `LogAndReturnException` with `Response.HasStarted == true` |
| **Enforcement** | `ErrorHandler.IsTooHotToHandle` |
| **Outcome** | Warning logged, exception propagates, and no headers or body are changed |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs` |
| **Operations** | [Log an exception and write the HTTP error response](operations/log-and-return-exception.md), [Legacy conventional middleware](operations/legacy-invoke-middleware.md) |

---

## EH-BR-02 Every handled HTTP error is logged at Error level

| Field | Value |
| --- | --- |
| **Statement** | Every handled HTTP error is logged at Error level with the exception and its message, and the logger's returned ID becomes the error's correlation ID. |
| **Trigger** | Any `LogAndReturnException` call |
| **Enforcement** | `ErrorHandler.LogAndReturnException` |
| **Outcome** | `ErrorID` is set to the logger's returned log ID |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs` |
| **Operations** | [Log an exception and write the HTTP error response](operations/log-and-return-exception.md) |

---

## EH-BR-03 Every error response carries Error-ID and Server-Name headers

| Field | Value |
| --- | --- |
| **Statement** | Every error response carries an `Error-ID` header (the log ID) and a `Server-Name` header (the host machine name). |
| **Trigger** | Any `LogAndReturnException` call |
| **Enforcement** | `ErrorHandler.LogAndReturnException` |
| **Outcome** | Client can correlate with the logs; machine name is disclosed |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs` |
| **Operations** | [Log an exception and write the HTTP error response](operations/log-and-return-exception.md) |

---

## EH-BR-04 Status code and content type come exactly from the caller

| Field | Value |
| --- | --- |
| **Statement** | The response status code and content type are exactly those supplied by the caller. For `HttpStatusCodeException` they come from the exception; otherwise they are 500 / `text/plain`. |
| **Trigger** | `LogAndReturnException` |
| **Enforcement** | `ErrorHandler.LogAndReturnException`; values chosen in `ErrorHandler.Invoke` (legacy) or `DynamicErrorHandler.InvokeAsync` (ServiceExtensions) |
| **Outcome** | Status/content type set |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs` / `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cs` |
| **Operations** | [Log an exception and write the HTTP error response](operations/log-and-return-exception.md), [Legacy conventional middleware](operations/legacy-invoke-middleware.md) |

---

## EH-BR-05 Handled-exception body format depends on content type

| Field | Value |
| --- | --- |
| **Statement** | For a handled exception, a non-`text/plain` content type (for example JSON or XML) gets the raw `ex.Message` as the body so the payload format is preserved. A `text/plain` content type gets the message followed by `"\nPlease see error: {ErrorID}."` |
| **Trigger** | `isHandled == true` |
| **Enforcement** | `ErrorHandler.BuildHandledResponseMessage` |
| **Outcome** | Body format depends on the content type |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs` |
| **Operations** | [Log an exception and write the HTTP error response](operations/log-and-return-exception.md) |

---

## EH-BR-06 Unhandled exception produces a generic-prefixed body

| Field | Value |
| --- | --- |
| **Statement** | An unhandled exception produces the body `"Oops! Something went wrong.\n{ex.Message}\nPlease see error: {ErrorID}."` |
| **Trigger** | `isHandled == false` |
| **Enforcement** | `ErrorHandler.BuildUnhandledResponseMessage` |
| **Outcome** | Generic prefix, but the raw exception message is still disclosed |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs` |
| **Operations** | [Log an exception and write the HTTP error response](operations/log-and-return-exception.md) |

---

## EH-BR-07 The legacy middleware requires a non-null next delegate

| Field | Value |
| --- | --- |
| **Statement** | The legacy middleware requires a non-null next delegate. |
| **Trigger** | `Invoke` with `_next == null` |
| **Enforcement** | `ErrorHandler.Invoke` |
| **Outcome** | `ArgumentNullException` is caught by its own generic handler, giving an unhandled 500 response |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs` |
| **Operations** | [Legacy conventional middleware](operations/legacy-invoke-middleware.md) |

---

## EH-BR-08 Exception classification precedence for HTTP hosts

| Field | Value |
| --- | --- |
| **Statement** | Exception classification precedence: `HttpStatusCodeException` (handled, own status and content type, log details), then `SAIFException` (handled, 500, text/plain, log details), then any other exception (unhandled, 500, text/plain). |
| **Trigger** | Exception from the downstream pipeline |
| **Enforcement** | `ErrorHandler.Invoke` (legacy); re-implemented in `DynamicErrorHandler.InvokeAsync` (ServiceExtensions) |
| **Outcome** | Selects the `isHandled`, status, and content type for the response |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs` / `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cs` |
| **Operations** | [Legacy conventional middleware](operations/legacy-invoke-middleware.md) |

---

## EH-BR-09 Windows Service stack trace flattens the inner-exception chain

| Field | Value |
| --- | --- |
| **Statement** | A Windows Service error's stack trace includes every exception in the inner-exception chain, each prefixed by `TypeName: Message`. |
| **Trigger** | `HandleError` with a non-null `ex` |
| **Enforcement** | `ExceptionExtensions.StackTrace`; called at `WindowsServiceErrorHandler.GetWindowsServiceError` |
| **Outcome** | Flattened trace in the logged text |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ExceptionExtensions.cs` / `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/WindowsServiceErrorHandler.cs` |
| **Operations** | [Handle a Windows Service error](operations/handle-windows-service-error.md), [Exception utilities](operations/exception-utilities.md) |

---

## EH-BR-10 SAIFException log details are copied into the Windows Service error

| Field | Value |
| --- | --- |
| **Statement** | `SAIFException.LogDetails` entries are copied, as strings, into the Windows Service error's log details. Other exception types contribute none. |
| **Trigger** | `HandleError` with `ex is SAIFException` |
| **Enforcement** | `ExceptionExtensions.GetLogDetailsForSAIfException`; `WindowsServiceErrorHandler.GetWindowsServiceError` |
| **Outcome** | "Log Details:" section in the logged text. A null `LogDetails` throws. |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ExceptionExtensions.cs` / `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/WindowsServiceErrorHandler.cs` |
| **Operations** | [Handle a Windows Service error](operations/handle-windows-service-error.md), [Exception utilities](operations/exception-utilities.md) |

---

## EH-BR-11 A BeginErrorHandling subscriber may suppress logging or actions

| Field | Value |
| --- | --- |
| **Statement** | A `BeginErrorHandling` subscriber may suppress logging (`Handled = true`) and/or suppress special actions (`FireSpecialActions = false`). By default both run. |
| **Trigger** | `HandleError` |
| **Enforcement** | `WindowsServiceErrorHandler.HandleError`; defaults in ServiceContracts `BeginErrorHandlingEventArgs` |
| **Outcome** | Log and/or actions skipped |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/WindowsServiceErrorHandler.cs` / `src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/BeginErrorHandlingEventArgs.cs` |
| **Operations** | [Handle a Windows Service error](operations/handle-windows-service-error.md) |

---

## EH-BR-12 Special actions run in registration order and continue after a Fail result only if failure logging succeeds

| Field | Value |
| --- | --- |
| **Statement** | When special actions fire, each registered action runs in order, and execution proceeds to the next action after a `Fail` result only if the Error-level failure-logging call for that result does not itself throw. A thrown fire-condition predicate, a custom `ISpecialErrorAction.Invoke` override that throws, or a throwing logger call all stop the loop early, preventing any remaining actions — and the subsequent `ErrorHandled` event — from running. |
| **Trigger** | `FireSpecialActions == true` |
| **Enforcement** | `WindowsServiceErrorHandler.HandleError` |
| **Outcome** | All results collected only when no action, predicate, or failure-logging call throws; otherwise the loop and `HandleError` itself terminate early |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/WindowsServiceErrorHandler.cs` / `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/SpecialErrorAction.cs` |
| **Operations** | [Handle a Windows Service error](operations/handle-windows-service-error.md) |

---

## EH-BR-13 A failed special action is logged at Error level

| Field | Value |
| --- | --- |
| **Statement** | A failed special action is logged at Error level as `"Special Error Action Failed: {Title}"` with the action's exception. |
| **Trigger** | Action result `Status == Fail` |
| **Enforcement** | `WindowsServiceErrorHandler.HandleError` |
| **Outcome** | Extra error log entry |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/WindowsServiceErrorHandler.cs` |
| **Operations** | [Handle a Windows Service error](operations/handle-windows-service-error.md) |

---

## EH-BR-14 ErrorHandled is raised after handling completes without an earlier escape

| Field | Value |
| --- | --- |
| **Statement** | After the special-actions loop finishes, `ErrorHandled` is raised with the exception, the `Handled` flag, all collected action results, and the context container — but only if nothing earlier in `HandleError` (the `BeginErrorHandling` subscribers, the initial Error-level log call, a fire-condition predicate, a custom action, or a failure-logging call) threw and escaped first (see [EH-BR-12](#eh-br-12-special-actions-run-in-registration-order-and-continue-after-a-fail-result-only-if-failure-logging-succeeds)). |
| **Trigger** | End of `HandleError`, reached only when no earlier step threw |
| **Enforcement** | `WindowsServiceErrorHandler.OnErrorHandled` |
| **Outcome** | Subscribers notified when `HandleError` runs to completion; otherwise the exception propagates out of `HandleError` and no `ErrorHandled` event fires |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/WindowsServiceErrorHandler.cs` |
| **Operations** | [Handle a Windows Service error](operations/handle-windows-service-error.md) |

---

## EH-BR-15 A typed special action fires only on an exact type match

| Field | Value |
| --- | --- |
| **Statement** | A typed special action fires only when the exception's runtime type is exactly `T`. Subclasses do not match, and a null exception never matches. |
| **Trigger** | `SpecialErrorAction<T>.Invoke` |
| **Enforcement** | `SpecialErrorAction<T>.Invoke` |
| **Outcome** | `NotExectued` result when there is no match |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/SpecialErrorAction.cs` |
| **Operations** | [Execute a special error action](operations/execute-special-error-action.md) |

---

## EH-BR-16 A special action fires only when its fire-condition predicate allows it

| Field | Value |
| --- | --- |
| **Statement** | A special action fires only when its optional fire-condition predicate is absent or returns true for the context container. |
| **Trigger** | `SpecialErrorAction.Invoke` |
| **Enforcement** | `SpecialErrorAction.Invoke` |
| **Outcome** | `NotExectued` result when the predicate is false |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/SpecialErrorAction.cs` |
| **Operations** | [Execute a special error action](operations/execute-special-error-action.md) |

---

## EH-BR-17 The built-in action wrapper catches only its own wrapped delegate

| Field | Value |
| --- | --- |
| **Statement** | The base `SpecialErrorAction.Invoke` catches an exception only from the wrapped `invoke` delegate call itself, returning it as a `Fail` result. The fire-condition predicate (`testFireCondition`) is evaluated **outside** that try block, so a throwing predicate propagates out of `Invoke` uncaught. A custom type that implements `ISpecialErrorAction`/overrides `Invoke` directly (such as `SpecialErrorAction<T>`'s own overridden `Invoke` body before it delegates to the base) is not bound by this containment at all. |
| **Trigger** | Delegate throws (caught); predicate or an overriding `Invoke` throws (not caught) |
| **Enforcement** | `SpecialErrorAction.Invoke` |
| **Outcome** | `Success`/`Fail` result for a wrapped-delegate exception; an uncaught exception for a predicate or non-conforming override, which stops the enclosing action loop (see [EH-BR-12](#eh-br-12-special-actions-run-in-registration-order-and-continue-after-a-fail-result-only-if-failure-logging-succeeds)) |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/SpecialErrorAction.cs` |
| **Operations** | [Execute a special error action](operations/execute-special-error-action.md) |

---

## EH-BR-18 Windows Service error ID format and lazy generation

| Field | Value |
| --- | --- |
| **Statement** | A Windows Service error ID is `{GUID}-{yyyyMMddHHmmssffffzzz}` with `:` replaced by `-`. It is generated once, on first access, and that access stamps `ErrorTime`. |
| **Trigger** | First read of `WindowsServiceError.ID` |
| **Enforcement** | `WindowsServiceError.ID` / `BuildErrorId` |
| **Outcome** | Stable ID per error object |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/WindowsServiceError.cs` |
| **Operations** | [Format a Windows Service error](operations/format-windows-service-error.md) |

---

## EH-BR-19 Logged Windows Service error text always includes a fixed field set

| Field | Value |
| --- | --- |
| **Statement** | The logged Windows Service error text always includes the ID, current time, application, host, track, machine, `user@domain`, source, exception name, message, and stack trace, plus log details when present. |
| **Trigger** | Logging step of `HandleError` |
| **Enforcement** | `WindowsServiceError.GetFormattedErrorString` |
| **Outcome** | Standard multi-line error text |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/WindowsServiceError.cs` |
| **Operations** | [Handle a Windows Service error](operations/handle-windows-service-error.md), [Format a Windows Service error](operations/format-windows-service-error.md) |

---

## EH-BR-20 Context objects are unique by key, first registration wins

| Field | Value |
| --- | --- |
| **Statement** | Context objects are unique by key, and the first registration wins. Later adds with the same key are silently ignored. |
| **Trigger** | `ContextContainer.AddContextObject` |
| **Enforcement** | `ContextContainer.AddContextObject(string, object)` |
| **Outcome** | No overwrite and no error |
| **Source** | `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/ContextContainer.cs` |
| **Operations** | [Pass error context to actions](operations/pass-error-context-to-actions.md) |

## Rules enforced outside this library over this library's data
- **Shutdown exemption.** The service stops unless the exception's exact type is listed in `ShutdownExemptExceptionTypes` and that entry's `IsAllowed(ex)` returns true. This is enforced by `SAIF.NetCore.WindowsService.ServiceWorker.IsExceptionExemptFromShutdown` (`src/SAIF.NetCore.WindowsService/SAIF.NetCore.WindowsService/SAIF.NetCore.WindowsService/ServiceWorker.cs`), not by this library.
- **Hook failure aggregation.** If any `IErrorHandlerHook` throws, `ErrorHandler` receives an `AggregateException` (hook exceptions plus the original) instead of the original. This is enforced by `SAIF.NetCore.ServiceExtensions.Middleware.DynamicErrorHandler.LogAndReturnException` (`src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cs`), not by this library.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [API reference](api/index.md)

## Sources
- `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs`
- `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ExceptionExtensions.cs`
- `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/WindowsServiceErrorHandler.cs`
- `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/SpecialErrorAction.cs`
- `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/WindowsServiceError.cs`
- `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/WindowsService/ContextContainer.cs`
- `src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/BeginErrorHandlingEventArgs.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cs`
- `src/SAIF.NetCore.WindowsService/SAIF.NetCore.WindowsService/SAIF.NetCore.WindowsService/ServiceWorker.cs`
