Skip to content

Example: SOAP service faults

faults.md documents how failures leave a WCF service: the declared fault contracts, the service behaviors and error handlers that shape a fault, and what a caller actually receives for each operation. Doc gen produces one per service at docs/services/<service-slug>/faults.md.

What reviewers should check

  • The opening paragraph states the caller-visible outcome, not the mechanism: a typed fault, a generic fault, or no fault at all. Here all three occur, and the page says which operation produces each.
  • Faults are SOAP faults. No HTTP status code, problem-details body, or exception-to-status mapping appears anywhere on the page.
  • The fault surface summary counts agree with the rest of the page: one declared fault contract, one service class carrying [LogErrorBehavior], one IErrorHandler implementation.
  • Service behaviors come from both sources: attributes in code and <serviceBehaviors> in configuration. The page names the behavior configuration that binds to the service, and says whether includeExceptionDetailInFaults is on, since that decides whether detail crosses the boundary.
  • The error handler section states what HandleError returns and what ProvideFault does to the fault. Returning false means the exception is not considered handled; a reviewer should not read "logged" as "converted".
  • Logged fields come from the handler's code, and no credential or message body is listed. An unobservable log destination reads "Not determined from source" and is not guessed.
  • The Mermaid diagram matches the prose: the typed-fault branch, the default fault branch, and the separate logging branch all appear, with no node the text does not mention.
  • Declared faults and failure modes agree with the contract page. SubmitClaim declares ClaimValidationFault in both places, and GetClaimStatus declares none in both.
  • Swallowed failures are listed with a file and line, because they are the places where a caller cannot tell success from failure. A one-way operation failure belongs in the failure modes table even though the caller sees nothing.
  • Links to operation sections go to the contract page with an anchor that matches the lowercase operation name, and ## Related documentation links root pages as siblings.

Example

The document below is a complete, correct service-faults.md for a fictional WCF service.

View rendered example — the same document as a real page, so you can compare rendering as well as source.

---
title: Faults and error pipeline
summary: Fault contracts, error handlers, service behaviors, and what a caller receives when an operation fails.
status: draft
owner: Claims Systems Team
last_reviewed: 2026-09-10
---

# Faults and error pipeline

A failure in `ClaimIntake` reaches the caller in one of three ways. A validation failure in `SubmitClaim` is thrown as `FaultException<ClaimValidationFault>` and arrives as a typed SOAP fault. Any other exception thrown by `SubmitClaim` arrives as WCF's default generic fault, with no exception detail because `includeExceptionDetailInFaults` is false. A failure in `GetClaimStatus` never reaches the caller as a fault: the method catches it and returns null. In every case a `[LogErrorBehavior]` error handler logs the exception first and leaves the fault itself untouched.

## Fault surface summary

| Aspect | Finding |
| --- | --- |
| Declared fault contracts | 1 - `ClaimValidationFault` on `SubmitClaim` |
| Service behavior attributes | `[LogErrorBehavior]` on 1 service class |
| `IErrorHandler` implementations | `ClaimIntakeErrorHandler` (`src/SAIF.Claims.Intake.Service/Behaviors/ClaimIntakeErrorHandler.cs`) |
| `includeExceptionDetailInFaults` | false |
| Effect on callers | A caller sees a typed `ClaimValidationFault` for a failed validation, a generic fault with no detail for any other exception, and a null result when `GetClaimStatus` fails |

## Service behaviors

| Behavior | Applied to | Declared in | Effect |
| --- | --- | --- | --- |
| `[LogErrorBehavior]` | `ClaimIntake` | `src/SAIF.Claims.Intake.Service/Behaviors/LogErrorBehaviorAttribute.cs` | Installs `ClaimIntakeErrorHandler` on every channel dispatcher through `ApplyDispatchBehavior` |
| `<serviceDebug includeExceptionDetailInFaults="false"/>` | `ClaimIntake` (`behaviorConfiguration="ClaimIntakeBehavior"`) | `Web.config` `<serviceBehaviors>` | Exception type, message, and stack trace are not included in generic faults |
| `<serviceMetadata httpGetEnabled="true"/>` | `ClaimIntake` (`behaviorConfiguration="ClaimIntakeBehavior"`) | `Web.config` `<serviceBehaviors>` | The WSDL is published at `ClaimIntake.svc?wsdl` |

The code attribute and the configuration behavior do not interact: the attribute adds an error handler, and `serviceDebug` only controls whether WCF puts exception detail into a generic fault.

## Error handler

| Field | Value |
| --- | --- |
| **Type** | `ClaimIntakeErrorHandler` : `IErrorHandler` |
| **Source** | `src/SAIF.Claims.Intake.Service/Behaviors/ClaimIntakeErrorHandler.cs` |
| **Installed by** | `LogErrorBehaviorAttribute` : `IServiceBehavior` - `ApplyDispatchBehavior` |
| **Scope** | Every channel dispatcher on every service carrying the attribute |
| **`HandleError` return** | `false` - the exception is not considered handled |
| **`ProvideFault` behavior** | Leaves the fault unchanged - a thrown `FaultException<ClaimValidationFault>` is sent as the typed fault and every other exception produces the default fault |

`HandleError` writes one error entry through the log surface that records the exception type, message, stack trace, the SOAP action of the failing call, and the Windows identity of the caller, then returns `false`. Returning `false` means the exception continues through the WCF pipeline, so the fault the caller receives is whatever WCF would have produced without the handler.

### Logged exception detail

| Field | Value written | Source of the value |
| --- | --- | --- |
| Exception type | Full type name | `error.GetType().FullName` |
| Message | Exception message | `error.Message` |
| Stack trace | Full stack trace | `error.StackTrace` |
| SOAP action | Action of the failing request | `OperationContext.Current.IncomingMessageHeaders.Action` |
| Caller identity | Windows account name | `ServiceSecurityContext.Current.WindowsIdentity.Name` |

| Aspect | Finding |
| --- | --- |
| Log surface | `ILogService` |
| Destination | Not determined from source |
| Failure of the logger itself | The logging call is wrapped in a try/catch that discards the exception, so a logging failure is silent and the fault path is unaffected |

## Error pipeline diagram

The following diagram visualizes how an exception becomes a fault.

```mermaid
flowchart TD
    OP["Operation throws<br>an exception"] --> DISP["Channel dispatcher"]
    DISP --> PROVIDE["ClaimIntakeErrorHandler.ProvideFault<br>leaves the fault unchanged"]
    PROVIDE --> KIND{"Exception type"}
    KIND -->|"FaultException of ClaimValidationFault"| TYPED["Typed fault ClaimValidationFault"]
    KIND -->|"Any other exception"| DEFAULT["WCF produces the default fault<br>with no exception detail"]
    TYPED --> CALLER["Caller receives SOAP fault"]
    DEFAULT --> CALLER
    DISP --> HANDLER["ClaimIntakeErrorHandler.HandleError"]
    HANDLER --> LOG["Exception detail logged<br>via ILogService"]
    HANDLER --> RET{"HandleError returns"}
    RET -->|"false"| NOTHANDLED["Exception not considered handled<br>WCF pipeline continues"]
```

## Declared fault contracts

| Fault type | Declared on | Members | Raised when |
| --- | --- | --- | --- |
| `ClaimValidationFault` | [`SubmitClaim`](contracts/claim-intake.md#submitclaim) | `Code`, `FieldErrors`, `Message` | The injury date is in the future ([BR-CLM-001](business-rules.md#br-clm-001-injury-date-not-in-future)) or the employer policy is not active on the injury date ([BR-CLM-002](business-rules.md#br-clm-002-policy-active-on-injury-date)) |

## Failure modes by operation

| Operation | Failure | What the caller receives | Detail exposed |
| --- | --- | --- | --- |
| [`SubmitClaim`](contracts/claim-intake.md#submitclaim) | Injury date in the future or policy not active on the injury date | Typed fault `ClaimValidationFault` | `Code`, `Message`, and the list of failing fields |
| [`SubmitClaim`](contracts/claim-intake.md#submitclaim) | `PKG_CLAIM_INTAKE.INSERT_CLAIM` fails with `OracleException` | Generic SOAP fault | None |
| [`GetClaimStatus`](contracts/claim-intake.md#getclaimstatus) | No claim matches the claim number | Default value with no fault | None |
| [`GetClaimStatus`](contracts/claim-intake.md#getclaimstatus) | Any exception during the lookup | Default value with no fault | None |
| [`FlagClaimForReview`](contracts/claim-intake.md#flagclaimforreview) | `PKG_CLAIM_INTAKE.ADD_REVIEW_FLAG` fails with `OracleException` | Nothing - a one-way operation returns no message and the failure is only logged | None |

## Swallowed failures

| Location | Behavior | Consequence |
| --- | --- | --- |
| `ClaimIntake.GetClaimStatus` (`src/SAIF.Claims.Intake.Service/ClaimIntake.svc.cs:104`) | A `catch (Exception)` block logs the exception through `ILogService` and returns null without rethrowing | The caller cannot distinguish a database failure from an unknown claim number, and `ClaimIntakeErrorHandler` never sees the exception |

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Activation and hosting](activation-hosting.md)
- [Operations](operations.md)
- [Authentication and authorization](authentication-authorization.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Data model](data-model.md)
- [Operational runbook](operational-runbook.md)
- [Tech debt](tech-debt.md)

## Sources
- `src/SAIF.Claims.Intake.Service/Behaviors/LogErrorBehaviorAttribute.cs`
- `src/SAIF.Claims.Intake.Service/Behaviors/ClaimIntakeErrorHandler.cs`
- `src/SAIF.Claims.Intake.Service/ClaimIntake.svc.cs`
- `src/SAIF.Claims.Intake.Service/DataContracts/ClaimValidationFault.cs`
- `src/SAIF.Claims.Intake.Service/Web.config`