Skip to content
DomainKit
Esc
navigateopen⌘Jpreview
On this page

Failure reasons

The one error type DomainKit raises, the sixteen reasons it carries, the HTTP status each answers with, and what a host should do about each of them.

Every DomainKit operation fails with one DomainKit.Error. It carries exactly one Reason, and category, isRetryable, and httpStatus all derive from that reason. Hosts match on the reason; nothing parses a message.

import { DomainKit, Reason } from "domainkit";

The reasons

Reason Status Retryable Carries Next step
InvalidInput 400 no message, field Fix the request
Unauthenticated 401 no message Sign in, or supply a working credential
Forbidden 403 no message The credential lacks the permission
Reconnect 403 no provider, connectionId Ask the customer to connect the provider again
NotFound 404 no entity, id The row or provider object does not exist for this owner
Conflict 409 no planId, operations Show the conflicting records; approve partially or fix DNS
Stale 409 no planId, digest The zone moved under the plan; build a new one
Expired 409 no entity, id The plan, approval, continuation, or credential aged out
Busy 409 yes key Another apply or refresh holds the lock; retry
ProviderConflict 409 no provider, code, message The provider refused because a conflicting record exists
Unsupported 501 no provider, operation, message The provider or that target cannot do it
ProviderRejected 502 no provider, code, message The provider refused; the message says why
ProviderUnavailable 503 yes provider, retryAfterMs Rate limited or down; retry after the delay
StorageFailed 500 yes operation, message The database call failed; retry
CryptoFailed 500 no operation Sealing or opening failed; check the custody key
ResolverFailed 500 no resolver, message The DNS pool could not be reached

Categories

category groups the reasons for logging and metrics: request, auth, plan, provider, storage, internal.

Matching

/** Every failure is one `DomainKit.Error`; the reason says what to do next. */
export const explain = applyAndSummarise.pipe(
  Effect.catchTag("DomainKitError", (error) =>
    Match.value(error.reason).pipe(
      Match.tag("Conflict", ({ operations }) =>
        Effect.succeed(`Fix ${operations.length} conflicting record(s) first`),
      ),
      Match.tag("Stale", () => Effect.succeed("The zone moved; build a new plan")),
      Match.tag("Expired", () => Effect.succeed("The plan aged out; build a new one")),
      Match.orElse(() => Effect.succeed(error.message)),
    ),
  ),
);

DomainKit.isError(value) narrows an unknown to the error, for a catch at a foreign boundary.

Over the wire

domainkit/server answers with the error value itself at the status its reason derives, so a Conflict is a 409 whose body still carries the conflicting operations and a Reconnect is a 403 naming the connection. domainkit/client decodes that body back into the same DomainKit.Error, reason intact.

A reply the client cannot read as one came from in front of the server: a proxy, a login page, a maintenance window. Those are classified from the status instead, and always name the transport’s base URL as the origin. The transport carries that table.

In the UI

@domainkit/react renders a sentence per reason from Messages.Catalog, so nothing shows a tag. Override one key to change one sentence.

/** A failure keeps the `DomainKit.Error`, so read `reason`, `category`, and `isRetryable`. */
export function ConnectionProblem({ domain }: { readonly domain: string }) {
  const { messages } = DomainKit.useDomainKit();
  const connection = Connect.useController({ domain });
  const state = connection.state;
  if (state._tag !== "Failure") return null;
  return (
    <p role="alert">
      {Messages.failure(state.error, messages)}
      {state.error.isRetryable ? (
        <button onClick={connection.retry} type="button">
          Try again
        </button>
      ) : null}
    </p>
  );
}

Troubleshooting covers what to do when one of these keeps happening.

Was this page helpful?