Skip to content

Failures

Handle errors consistently across all IdentityScribe channels. This reference covers error response parsing, retry logic, user-friendly messages, and troubleshooting via logs and traces.

Related:

Every error can be represented by these fields. Where they appear depends on the transport: body fields (HTTP/GraphQL), headers/trailers (HTTP/gRPC), or diagnostic messages (LDAP).

FieldTypePresenceStabilityDescription
idstring (UUID)AlwaysStableUnique identifier for this failure instance
timestampstring (ISO 8601)AlwaysStableWhen the failure occurred
trace_idstringOptionalStableOpenTelemetry trace ID (32 hex chars) where the failure was created
span_idstringOptionalStableOpenTelemetry span ID (16 hex chars) where the failure was created
codestringAlwaysStableMachine-readable error code (e.g., ARGUMENT_INVALID_JSON)
kindstringAlwaysStableError classification (e.g., INVALID_ARGUMENT)
messagestringAlwaysInformationalHuman-readable description (may change between versions)
correlationstringOptionalStableRequest correlation ID (when provided by client)
retryobjectOptionalStableRetry hint: { "after": "PT2S" } or { "at": "..." }. Absent when retry is not suggested.
detailsobjectOptionalStableStructured context specific to the error code. Absent for generic failures.

Default codes: When no explicit code is set, the default is the kind name (e.g., INVALID_ARGUMENT). For the canonical list of codes, see the generated Error Catalog.

Clients should parse and act on these fields:

  • id: Unique failure identifier for correlation with logs/traces
  • timestamp: When the failure occurred (ISO 8601)
  • trace_id: OpenTelemetry trace ID for distributed tracing correlation (optional, present when tracing is active)
  • span_id: OpenTelemetry span ID for distributed tracing correlation (optional, present when tracing is active)
  • code: Stable error code for programmatic handling (switch/match statements)
  • kind: Error classification for category-level handling
  • correlation: Request correlation ID (optional, present when client provides one)
  • retry: Machine-readable retry semantics (optional, present when retry is suggested)
  • details: Structured context (optional, schema is stable per error code when present)
  • message: User-facing description derived from the error. May change between versions; do not parse programmatically.

For details schemas:

  • Additive fields allowed: New optional fields may be added
  • No removals: Existing fields are never removed
  • No renames: Field names are stable
  • Semantics preserved: Field meanings do not change

IdentityScribe uses 16 error kinds aligned with gRPC status codes:

KindMeaningDefault Retry
CANCELLEDOperation was cancelled (typically by the caller)Never
INVALID_ARGUMENTInvalid request parameters (malformed or semantically invalid)Never
OUT_OF_RANGERequested value is out of rangeNever
FAILED_PRECONDITIONOperation not allowed in the current stateNever
UNAUTHENTICATEDMissing or invalid authentication credentialsNever
PERMISSION_DENIEDCaller does not have permissionNever
NOT_FOUNDRequested resource was not foundNever
ALREADY_EXISTSResource already existsNever
CONFLICTRequest conflicts with current resource stateNever
RESOURCE_EXHAUSTEDQuota, limit, or capacity exhaustedAfter 2s
DEADLINE_EXCEEDEDOperation timed outAfter 1s
UNAVAILABLEService temporarily unavailableAfter 5s
UNIMPLEMENTEDOperation not implemented or supportedNever
INTERNALInternal error (invariant broken)Never
DATA_LOSSUnrecoverable data loss or corruptionNever
UNKNOWNUnknown or unmapped errorNever
KindHTTP Status
CANCELLED499 (Client Closed Request)
INVALID_ARGUMENT400 (Bad Request)
OUT_OF_RANGE400 (Bad Request)
FAILED_PRECONDITION409 (Conflict)
UNAUTHENTICATED401 (Unauthorized)
PERMISSION_DENIED403 (Forbidden)
NOT_FOUND404 (Not Found)
ALREADY_EXISTS409 (Conflict)
CONFLICT409 (Conflict)
RESOURCE_EXHAUSTED429 (Too Many Requests)
DEADLINE_EXCEEDED504 (Gateway Timeout)
UNAVAILABLE503 (Service Unavailable)
UNIMPLEMENTED501 (Not Implemented)
INTERNAL500 (Internal Server Error)
DATA_LOSS500 (Internal Server Error)
UNKNOWN500 (Internal Server Error)
HeaderDescription
Error-IdUnique failure identifier (UUID)
Error-CodeStable error code
Error-KindError classification
Correlation-IdRequest correlation ID (if present)
Trace-IdOpenTelemetry trace ID (32 hex chars, if tracing active)
Span-IdOpenTelemetry span ID (16 hex chars, if tracing active)
Retry-AfterRetry delay in seconds or HTTP-date

Naming Convention: HTTP headers use Title-Case (Trace-Id, Span-Id) per HTTP conventions, while JSON body fields use snake_case (trace_id, span_id) to match OpenTelemetry and logging conventions.

The response body is wrapped in an error object. The error.status field records the HTTP status that would have been returned (useful for streaming/GraphQL cases where the HTTP status may already be committed as 200).

{
"error": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-01-07T10:30:00Z",
"trace_id": "0af7651916cd43dd8448eb211c80319c",
"span_id": "b7ad6b7169203331",
"code": "ARGUMENT_INVALID_JSON",
"kind": "INVALID_ARGUMENT",
"message": "Invalid JSON format for 'filter'.",
"status": 400,
"details": {
"location": "query",
"name": "filter",
"reason": "Invalid JSON syntax"
}
}
}
HTTP/1.1 400 Bad Request
Content-Type: application/json
Error-Id: 550e8400-e29b-41d4-a716-446655440000
Error-Code: ARGUMENT_INVALID_JSON
Error-Kind: INVALID_ARGUMENT
{
"error": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "ARGUMENT_INVALID_JSON",
"kind": "INVALID_ARGUMENT",
"message": "Invalid JSON format for 'filter'.",
"status": 400,
"details": {
"location": "query",
"name": "filter",
"reason": "Invalid JSON syntax",
"value": "{invalid"
}
}
}
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Error-Id: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Error-Code: DIRECTORY_BUSY
Error-Kind: UNAVAILABLE
Retry-After: 2
{
"error": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"code": "DIRECTORY_BUSY",
"kind": "UNAVAILABLE",
"message": "Directory service is busy. Please retry later.",
"status": 503,
"retry": {
"after": "PT2S"
},
"details": {
"permitsRequested": 1,
"permitsAvailable": 0,
"queueLength": 3,
"waitTimeMs": 5000
}
}
}

GraphQL responses use the GraphQL errors array. To keep error handling consistent across channels, failure details are carried under extensions.error using the same shape as the HTTP error object:

{
"errors": [
{
"message": "Invalid JSON format for 'filter'.",
"extensions": {
"error": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "ARGUMENT_INVALID_JSON",
"kind": "INVALID_ARGUMENT",
"message": "Invalid JSON format for 'filter'.",
"status": 400,
"details": {
"location": "query",
"name": "filter",
"reason": "Invalid JSON syntax"
}
}
}
}
]
}

Note: The GraphQL top-level message may be redundant; clients should treat extensions.error.code / .kind as the stable contract.

KindgRPC Status
CANCELLEDCANCELLED
INVALID_ARGUMENTINVALID_ARGUMENT
OUT_OF_RANGEOUT_OF_RANGE
FAILED_PRECONDITIONFAILED_PRECONDITION
UNAUTHENTICATEDUNAUTHENTICATED
PERMISSION_DENIEDPERMISSION_DENIED
NOT_FOUNDNOT_FOUND
ALREADY_EXISTSALREADY_EXISTS
CONFLICTABORTED
RESOURCE_EXHAUSTEDRESOURCE_EXHAUSTED
DEADLINE_EXCEEDEDDEADLINE_EXCEEDED
UNAVAILABLEUNAVAILABLE
UNIMPLEMENTEDUNIMPLEMENTED
INTERNALINTERNAL
DATA_LOSSDATA_LOSS
UNKNOWNUNKNOWN
TrailerPresenceDescription
error-idDefaultUnique failure identifier (UUID)
error-codeDefaultStable error code
correlation-idDefaultRequest correlation ID (when provided by client)
trace-idDefaultOpenTelemetry trace ID (32 hex chars, if tracing active)
span-idDefaultOpenTelemetry span ID (16 hex chars, if tracing active)
retry-afterDefaultRetry delay in seconds or RFC 1123 date (when retry is suggested)
error-kindOptionalError classification (not included by default)

The response includes google.rpc.Status with:

  • ErrorInfo: Always present. Contains reason (kind name), domain (k5), and metadata:
    • errorCode: Always included
    • errorDetails: Optional (not included by default; requires explicit configuration)
  • RetryInfo: Present when retry is suggested
Status {
code: 3 // INVALID_ARGUMENT
message: "Invalid JSON format for 'filter'."
details: [
ErrorInfo {
reason: "INVALID_ARGUMENT"
domain: "k5"
metadata: {
"errorCode": "ARGUMENT_INVALID_JSON"
}
}
]
}
KindLDAP ResultCode
CANCELLEDCANCELED (118)
INVALID_ARGUMENTPROTOCOL_ERROR (2)
OUT_OF_RANGECONSTRAINT_VIOLATION (19)
FAILED_PRECONDITIONCONSTRAINT_VIOLATION (19)
UNAUTHENTICATEDINVALID_CREDENTIALS (49)
PERMISSION_DENIEDINSUFFICIENT_ACCESS_RIGHTS (50)
NOT_FOUNDNO_SUCH_OBJECT (32)
ALREADY_EXISTSENTRY_ALREADY_EXISTS (68)
CONFLICTBUSY (51)
RESOURCE_EXHAUSTEDBUSY (51)
DEADLINE_EXCEEDEDTIME_LIMIT_EXCEEDED (3)
UNAVAILABLEUNAVAILABLE (52)
UNIMPLEMENTEDUNWILLING_TO_PERFORM (53)
INTERNALOTHER (80)
DATA_LOSSOTHER (80)
UNKNOWNOTHER (80)

Some error codes map to more specific LDAP result codes:

Error CodeLDAP ResultCode
DIRECTORY_SIZE_LIMIT_EXCEEDEDSIZE_LIMIT_EXCEEDED (4)
DIRECTORY_BUSYBUSY (51)

LDAP diagnostic messages include the error code and failure ID:

Directory service is busy. Please retry later. {"code":"DIRECTORY_BUSY","id":"7c9e6679-7425-40de-944b-e07fc1f90ae7"}

These errors indicate invalid request parameters.

CodeKindDescription
ARGUMENT_INVALID_JSONINVALID_ARGUMENTInvalid JSON syntax
ARGUMENT_INVALID_TYPEINVALID_ARGUMENTWrong JSON value type (e.g., expected array, got object)
ARGUMENT_INVALID_ELEMENTINVALID_ARGUMENTInvalid element type in array
ARGUMENT_MISSING_FIELDINVALID_ARGUMENTRequired field missing
ARGUMENT_MUTUALLY_EXCLUSIVEINVALID_ARGUMENTMutually exclusive parameters specified together
ARGUMENT_INVALID_VALUEINVALID_ARGUMENTInvalid value format
{
location?: string; // Where the argument appears: "query", "header", "body"
name?: string; // Parameter name
reason?: string; // Human-readable reason
value?: string; // Truncated input value (max 200 chars)
}
{
"error": {
"code": "ARGUMENT_INVALID_TYPE",
"kind": "INVALID_ARGUMENT",
"message": "Parameter 'include' must be array.",
"status": 400,
"details": {
"location": "query",
"name": "include",
"reason": "Expected ARRAY, got OBJECT"
}
}
}

These errors indicate problems parsing filter expressions (LDAP, SCIM, or JSON format).

CodeKindDescription
FILTER_INVALID_SYNTAXINVALID_ARGUMENTGeneral parse failure
FILTER_UNSUPPORTED_PATHINVALID_ARGUMENTSCIM attribute path not supported (e.g., dotted paths)
FILTER_UNSUPPORTED_FEATUREINVALID_ARGUMENTUnsupported filter feature
FILTER_NESTING_EXCEEDEDINVALID_ARGUMENTFilter nesting depth exceeds limit
FILTER_TOO_LARGEINVALID_ARGUMENTFilter string exceeds maximum length
{
format?: string; // Filter format: "JSON", "SCIM", "LDAP"
filter?: string; // Truncated filter string (max 200 chars)
reason?: string; // Human-readable reason
}
{
"error": {
"code": "FILTER_UNSUPPORTED_PATH",
"kind": "INVALID_ARGUMENT",
"message": "Unsupported attribute path in filter.",
"status": 400,
"details": {
"format": "SCIM",
"filter": "name.familyName eq \"Smith\"",
"reason": "SCIM attribute path 'name.familyName' is not supported. Only simple attribute names are allowed."
}
}
}

These errors are specific to directory service operations.

FieldValue
KindUNAVAILABLE
Default RetryAfter 2 seconds
DescriptionDirectory service is overloaded

Details Schema:

{
permitsRequested: number; // Permits the query tried to acquire
permitsAvailable: number; // Permits available at request time
queueLength: number; // Threads waiting for permits when rejected
waitTimeMs: number; // How long the request waited before failing
}

The queueLength field indicates how many other threads were waiting for permits when this request was rejected. High values suggest contention from slow queries holding permits.

FieldValue
KindRESOURCE_EXHAUSTED
Default RetryNever
DescriptionQuery result exceeded configured size limit

Details Schema:

{
sizeLimit: number; // Configured size limit
emitted: number; // Number of entries emitted before limit
phase: string; // Processing phase where limit was hit
}
FieldValue
KindDEADLINE_EXCEEDED
Default RetryAfter 1 second
DescriptionQuery exceeded configured time limit

Details Schema:

{
timeLimit: string; // Configured time limit (ISO 8601 duration)
phase: string; // Processing phase where limit was hit
}
FieldValue
KindINVALID_ARGUMENT
Default RetryNever
DescriptionQuery uses an unsupported operation for an attribute

Details Schema:

{
attributeOriginal: string; // Attribute name as specified in query
attributeNormalized: string; // Normalized attribute name
operation: string; // Unsupported operation: "SUBSTRING", "APPROXIMATE", "ORDERING", "SORT"
}
FieldValue
KindOUT_OF_RANGE
Default RetryNever
DescriptionQuery base DN is outside all configured directory scopes

Details Schema:

{
base: string; // Requested base DN
configuredBases: string[]; // Configured base DNs
}

Note: This response may reveal internal directory structure. Consider your security model when exposing configuredBases to external clients.

FieldValue
KindINVALID_ARGUMENT
Default RetryNever
DescriptionQuery base is too broad without an explicit type constraint

Details Schema:

{
base: string; // Requested base DN
candidates: string[]; // Type candidates that match the base
}
FieldValue
KindINVALID_ARGUMENT
Default RetryNever
DescriptionQuery constraints are inconsistent (base types vs filter types)

Details Schema:

{
base: string; // Requested base DN
baseTypes: string[]; // Types allowed by the base
intentTypes: string[]; // Types restricted by the filter
}
  1. Check retry field first: If present, use the specified delay
  2. Check Retry-After header: For HTTP, this header provides the delay
  3. Check kind: Only retry for transient kinds:
    • RESOURCE_EXHAUSTED (rate limiting)
    • DEADLINE_EXCEEDED (timeout)
    • UNAVAILABLE (service unavailable)
if error.retry.after:
wait(error.retry.after)
retry()
elif error.kind in [RESOURCE_EXHAUSTED, DEADLINE_EXCEEDED, UNAVAILABLE]:
wait(exponential_backoff())
retry()
else:
fail_permanently()
  • INVALID_ARGUMENT, OUT_OF_RANGE: Fix the request
  • UNAUTHENTICATED: Re-authenticate
  • PERMISSION_DENIED: Request access
  • NOT_FOUND: Resource doesn’t exist
  • ALREADY_EXISTS, CONFLICT: Resolve conflict
  • UNIMPLEMENTED: Feature not available
  • INTERNAL, DATA_LOSS, UNKNOWN: Contact support

When logging errors, include these fields for correlation:

FieldLog KeyPurpose
iderror_idCorrelate with traces and support tickets
codeerror_codeAggregate errors by type
kinderror_kindAggregate errors by category
correlationcorrelation_idTrace requests across services
trace_idtrace_idOpenTelemetry trace ID for distributed tracing
span_idspan_idOpenTelemetry span ID for distributed tracing
{
"level": "ERROR",
"message": "Request failed",
"error_id": "550e8400-e29b-41d4-a716-446655440000",
"error_code": "DIRECTORY_BUSY",
"error_kind": "UNAVAILABLE",
"correlation_id": "req-12345",
"trace_id": "0af7651916cd43dd8448eb211c80319c",
"span_id": "b7ad6b7169203331"
}

Some details fields may contain sensitive information:

  • configuredBases, baseTypes, intentTypes: May reveal internal directory structure
  • value, filter: May contain user input

Important: Details schemas are stable when present. However, deployments may omit sensitive fields or suppress details entirely depending on exposure policy. Clients should handle missing details gracefully.

When adding a new error code:

  1. Define the code: Add it to ScribeErrorCodes using SCREAMING_SNAKE_CASE without prefix (e.g., ARGUMENT_INVALID_JSON, FILTER_INVALID_SYNTAX, DIRECTORY_BUSY)
  2. Choose a kind: Select from the 16 available kinds
  3. Define details schema: Document all fields with types and descriptions
  4. Set retry semantics: Determine if the error is transient
  5. Update this guide: Add the code to the appropriate section
  6. Update the ADR: If the error represents a significant change to the error model