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.
Four outcome classes
| Domain outcome | Domain representation | Typical HTTP projection | Consumer action |
|---|---|---|---|
| Authenticated | AuthResult.Authenticated value | Success body plus committed session cookie | Enter application |
| Incomplete but successful flow | RequiresMfa, verification, approval, enrollment | Successful continuation body | Preserve flow data and call the next endpoint |
| Expected negative auth outcome | InvalidCredentials, disabled, policy denied value | Declared invalid-credentials or policy error | Display safe message or branch |
| Infrastructure/domain typed failure | Tagged Effect error | Declared specific error or sanitized internal error | Retry, map locally, or alert |
| Defect | Effect cause outside the typed error channel | Generic server failure | Trace, 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 outcome | Public result |
|---|---|
| Password sign-up duplicate identity | 409 identity_already_registered |
| Missing identity/user/credential on sign-in | Same invalid-credentials response, concealing which lookup failed |
| Storage, hashing, crypto, session failure | AuthInternalError with status 500 |
| Authentication continuation | Successful 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.