effect-auth

Error Model

Distinguish domain results, typed failures, public HTTP errors, and defects.

effect-auth keeps diagnostic failures at trusted layers and deliberately narrows public contracts. A storage cause helps Cloudflare Worker telemetry; a browser needs only a stable, safe error. Authentication outcomes add one nuance: some domain AuthResult values become HTTP errors, while others remain successful continuations.

HTTP Operations project domain outcomes onto a smaller public contract

Four outcome classes

Domain outcomeDomain representationTypical HTTP projectionConsumer action
AuthenticatedAuthResult.Authenticated valueSuccess body plus committed session cookieEnter application
Incomplete but successful flowRequiresMfa, verification, approval, enrollmentSuccessful continuation bodyPreserve flow data and call the next endpoint
Expected negative auth outcomeInvalidCredentials, disabled, policy denied valueDeclared invalid-credentials or policy errorDisplay safe message or branch
Infrastructure/domain typed failureTagged Effect errorDeclared specific error or sanitized internal errorRetry, map locally, or alert
DefectEffect cause outside the typed error channelGeneric server failureTrace, alert, and fix

RequiresExplicitLinking also exists in the domain result union, but no current standard HTTP transport maps it. Do not assume every new domain variant automatically belongs to the browser contract.

Domain errors are not public errors

Primitives expose tagged failures such as StorageError, PasswordHashError, and AuthFlowStateError:

password
  .signIn(input)
  .pipe(
    Effect.catchTag("StorageError", (error) =>
      Effect.zipRight(
        WorkerTelemetry.record("auth.storage_failed", error),
        Effect.fail(AppAuthUnavailable())
      )
    )
  );

Here WorkerTelemetry and AppAuthUnavailable are application services/errors. A typed error means the server can anticipate and handle it; it does not mean its entity, operation, message, cause, provider payload, or stack is safe to expose.

Defects are absent from Effect<A, E, R>'s E. Observe them at the Worker boundary with correlation data and return a generic response. Storage adapters should translate anticipated driver failures to StorageError, while programmer bugs may still defect.

HTTP Operation mapping

*HttpOperations apply request security, map domain outcomes/errors, and commit cookies. Examples:

Source outcomePublic result
Password sign-up duplicate identity409 identity_already_registered
Missing identity/user/credential on sign-inSame invalid-credentials response, concealing which lookup failed
Storage, hashing, crypto, session failureAuthInternalError with status 500
Authentication continuationSuccessful typed response variant

Standard auth errors are schema-backed classes such as AuthBadRequestError, AuthUnauthenticatedError, AuthPolicyDeniedError, and AuthRateLimitedError. They provide stable codes/statuses; rate-limited errors also expose retryAfterSeconds.

The schemas accept arbitrary message strings, so safety is a mapper/configuration responsibility rather than a type-level guarantee. Keep policy-denial reasons public-safe and never place account existence, secrets, SQL/provider details, or internal policy state in a message that can cross HTTP.

OAuth authorization/token/introspection/revocation endpoints use protocol-specific error and errorDescription classes rather than the normal auth code/message union. isAuthApiError narrows standard AuthHttpError, not every OAuth protocol error.

Schema failures

Decoding runs before handlers. AuthSchemaErrorMiddlewareLive maps failures to:

{
  "_tag": "AuthBadRequestError",
  "code": "bad_request",
  "message": "Invalid request",
  "issues": [{ "path": ["identity"], "code": "required" }]
}

At most ten normalized issues expose coarse codes such as required, invalid_type, or unexpected_key. A custom HttpApi must attach this middleware or explicitly define another schema-error contract and matching client. createAuthClient also runtime-decodes outgoing payloads, so invalid local input can reject before any network request and is not an AuthHttpError.

Browser handling on Cloudflare

With Alchemy routing auth through the app origin, handle decoded API errors separately from transport, cancellation, and decode failures:

try {
  return await auth.password.signIn({
    identity: { scope: { type: "global" }, kind: "email", value: email },
    password,
  });
} catch (error) {
  showError(
    isAuthApiError(error)
      ? authClientErrorMessage(error)
      : "Authentication is temporarily unavailable"
  );
}

Keep the unknown branch. authClientErrorMessage(unknown) may return an arbitrary object's or native Error message; use it directly only after narrowing to a trusted decoded public error. Successful responses still require a switch over type so MFA, verification, approval, or enrollment is not mistaken for full authentication.

Reusing an HTTP Operation preserves its mapping, but a Custom Auth API must declare compatible schemas. Continue with Browser Client.

On this page