SignatureKit
Signers

Typed error channel

Every SignatureKit failure is a SignatureKitError in Effect's error channel — discriminate by code with Effect.catchTag/catchIf, no throw.

Every failure in the core, the A1 signer, and the formats is a SignatureKitError in Effect's error channel — not a thrown exception. The return type carries it (Effect<SignatureArtifact, SignatureKitError>), so handling stays exhaustive and compiler-checked: you discriminate by code, and whatever you do not handle keeps propagating, typed.

Typed error channel

Failures are not thrown: they live in Effect's error channel as a SignatureKitError with a literal code. The compiler forces you to handle them — or let them propagate, typed — instead of discovering them at runtime.

The shape of the error

SignatureKitError is a class with a fixed _tag of "SignatureKitError". Its fields:

  • code — one of the 21 "signature-kit.*" literals from the catalog.
  • retryable — a boolean decided at the point of failure, not fixed per code. The same code may arrive with retryable: true at one call site and false at another.
  • reason? — an optional contextual message for logs and telemetry. UI copy should key off code, not parse this text.
  • operation? — a plain string naming the operation where the failure occurred, useful for logging and telemetry.
  • schemaName? — the name of the schema that failed to decode, when the failure came from Schema.decodeUnknownEffect.
  • issueMessage? — the formatted decode-issue text (String(issue)) for that schema failure.
  • provider? — the remote provider name, for provider-API failures (Clicksign, Assinafy, ZapSign, DocuSeal, Documenso).
  • status? — the upstream HTTP status code, when the failure came from a provider HTTP response.
  • retryAfterEpochSeconds? — an epoch-seconds hint for when to retry, when the upstream provided one (for example a rate-limit response).
  • get message() — the default English message for the code, or reason for codes marked editable.
error-shape.ts
import type { Effect } from "effect"
import type { SignatureArtifact, SignatureKitError, SignatureKitErrorCode } from "@signature-kit/signatures"

// The error channel is typed: each failure is a SignatureKitError.
declare const sign: Effect.Effect<SignatureArtifact, SignatureKitError>

// _tag: "SignatureKitError"
class SignatureKitError {
  readonly code: SignatureKitErrorCode   // 21 "signature-kit.*" literals
  readonly retryable: boolean         // decided at the point of failure
  readonly reason?: string            // contextual message for logs
  readonly operation?: string
  readonly schemaName?: string
  readonly issueMessage?: string
  readonly provider?: string
  readonly status?: number
  readonly retryAfterEpochSeconds?: number
  get message(): string               // default per code, or reason for editable codes
}

Do not treat retryable as a property of the code. Always read error.retryable from the received value — only that call site knows whether the failure is worth a retry.

Handling by code

The error union has a single tag, so Effect.catchTag("SignatureKitError", ...) enters the channel and you discriminate by error.code. A switch over code is exhaustive: the compiler requires all 21 literals.

handle-error.ts
import { Effect } from "effect"
import { a1SignaturesLayer } from "@signature-kit/a1/signer"
import { signatures } from "@signature-kit/signatures"

const program = Effect.gen(function* () {
  return yield* signatures.sign({ content, algorithm: "rsa-sha256" })
}).pipe(Effect.provide(a1SignaturesLayer({ pfx, password })))

// The error union is a single tag — discriminate by code, not by class.
const handled = program.pipe(
  Effect.catchTag("SignatureKitError", (error) => {
    switch (error.code) {
      case "signature-kit.WRONG_PASSWORD":
        return Effect.fail("Incorrect certificate password — ask for the password again.")
      case "signature-kit.SIGN_FAILED":
        // retryable is decided at the point of failure, not fixed per code.
        return error.retryable ? program : Effect.fail(error.message)
      default:
        return Effect.fail(error.message)
    }
  }),
)

To intercept one failure and let the rest propagate, use Effect.catchIf with a predicate over _tag and code:

catch-if.ts
import { Effect } from "effect"
import type { SignatureKitError } from "@signature-kit/signatures"

// Filter only the failure this call site knows how to handle; the rest propagate, typed.
const recovered = program.pipe(
  Effect.catchIf(
    (error): error is SignatureKitError =>
      error._tag === "SignatureKitError" && error.code === "signature-kit.WRONG_PASSWORD",
    (error) =>
      Effect.logWarning(`signing aborted at ${error.operation ?? "sign"}: ${error.message}`),
  ),
)

In retry handlers, decide based on the combination of error.code + error.retryable. For example, retry a signature-kit.SIGN_FAILED only when error.retryable is true.

Error catalog

All 21 SignatureKitError codes and their default messages. Codes marked editable resolve to reason ?? default in message. Each row is anchorable as #err-<CODE> (for example #err-WRONG_PASSWORD), so other pages deep link straight to a code.

CodeDefault message
signature-kit.EMPTY_FILECertificate file is empty (0 bytes).
signature-kit.INVALID_FORMATThe file is not a PKCS#12 (.pfx/.p12) certificate.· editable
signature-kit.INVALID_INPUTInvalid signing input.· editable
signature-kit.WRONG_PASSWORDWrong certificate password.
signature-kit.CERTIFICATE_EXPIREDCertificate expired.
signature-kit.CERTIFICATE_NOT_YET_VALIDCertificate is not valid yet.
signature-kit.MISSING_BR_IDENTIFIERCertificate does not contain a Brazilian CPF or CNPJ.
signature-kit.UNSUPPORTED_ALGORITHMThe certificate uses an unsupported encryption algorithm.· editable
signature-kit.NO_CERTIFICATEThe file does not contain a certificate.
signature-kit.NO_PRIVATE_KEYThe file does not contain a private key.
signature-kit.CORRUPTED_FILEThe file is corrupted or not a valid PKCS#12 certificate.
signature-kit.X509_PARSE_FAILEDX.509 parsing failed.· editable
signature-kit.PEM_EXTRACTION_FAILEDFailed to extract PEM material from the PFX.
signature-kit.KEY_IMPORT_FAILEDFailed to import the key into Web Crypto.· editable
signature-kit.DIGEST_FAILEDFailed to compute the certificate digest.
signature-kit.SIGN_FAILEDFailed to sign the content.· editable
signature-kit.VERIFY_FAILEDFailed to verify the signature.· editable
signature-kit.HTTPHTTP request failed.· editable
signature-kit.RESPONSE_SHAPEHTTP response shape was invalid.· editable
signature-kit.UNSUPPORTED_OPERATIONOperation is unsupported.· editable
signature-kit.UNKNOWNUnknown SignatureKit failure.· editable

Localized display messages

Do not localize by sniffing classes, loose .code fields, or substrings from reason. That path breaks when the diagnostic text changes:

do-not-sniff-reason.ts
export function getCertificateErrorMessage(error: { reason?: string }) {
  const reason = error.reason ?? ""
  if (reason.includes("not valid on the current date")) return "Certificado expirado"
  if (reason.includes("Brazilian CPF or CNPJ")) {
    return "Certificado não contém CPF ou CNPJ brasileiro"
  }

  return "Arquivo de certificado inválido"
}

Use @signature-kit/i18n at the application boundary instead. Pass only the catalogs your code can receive; the resolver checks _tag and code structurally and falls back to English when the requested locale is missing:

localized-error-message.ts
import { errorMessage } from "@signature-kit/i18n"
import { signatureKitErrorMessages } from "@signature-kit/signatures"
import { cryptoErrorMessages } from "@signature-kit/crypto/config"

const message = errorMessage(error, {
  locale: "pt-BR",
  catalogs: [signatureKitErrorMessages, cryptoErrorMessages],
  overrides: {
    "pt-BR": {
      "signature-kit.WRONG_PASSWORD": "Senha do certificado incorreta",
    },
  },
})

Format and provider errors

Beyond SignatureKitError, the format modules add their own typed families to the error channel. Provider APIs do not: invalid upstream request inputs, HTTP failures, response-shape failures, and unsupported operations stay in SignatureKitError.

XmlError

xml.* codes. signXml returns XmlError | SignatureKitError; verifyXml returns only XmlError (without the Signatures service).

PdfError

pdf.* codes. signPdf returns PdfError | CmsError | SignatureKitError. For example, hashAlgorithm: "sha384" fails as pdf.SIGN_FAILED — the current signer backend has no rsa-sha384 counterpart; "sha1" is accepted (mapped to rsa-sha1).

Provider APIs

No parallel family. Invalid input, remote HTTP, response shape, and unsupported operation fail as SignatureKitError with provider, operation, schemaName, and status when that metadata exists.
families.ts
import { Effect } from "effect"
import { signXml } from "@signature-kit/xml/sign"
import { xmlRuntimeLayer } from "@signature-kit/xml/runtime"
import { signPdf } from "@signature-kit/pdf/sign"
import { a1SignaturesLayer } from "@signature-kit/a1/signer"

const layer = a1SignaturesLayer({ pfx, password })

// signXml -> XmlError | SignatureKitError    (xml.* codes)
const xml = signXml({ xml: source, referenceId: "nfe-1" }).pipe(
  Effect.provide(layer),
  Effect.provide(xmlRuntimeLayer),
  Effect.catchTags({
    XmlError: (error) => Effect.fail(`Invalid XML: ${error.code}`),
    SignatureKitError: (error) => Effect.fail(`Signature: ${error.code}`),
  }),
)

// signPdf -> PdfError | CmsError | SignatureKitError    (pdf.* codes)
const pdf = signPdf({ pdf: bytes, policy: "pades-icp-brasil" }).pipe(
  Effect.provide(layer),
  Effect.catchTag("PdfError", (error) => Effect.fail(`PDF: ${error.code}`)),
)

For provider APIs, input, HTTP, and response-shape failures arrive as SignatureKitError. Discriminate by error.code the same way:

remote-signer-error.ts
import { Effect } from "effect"
import { ClicksignSignatureRequest } from "@signature-kit/clicksign"

const request = ClicksignSignatureRequest("contract", {
  title: "Contract",
  documents: documentProps,
  recipients,
}).pipe(
  Effect.catchTag("SignatureKitError", (error) => {
    switch (error.code) {
      case "signature-kit.HTTP":
        return Effect.fail(error.reason ?? "Remote HTTP failure.")
      case "signature-kit.INVALID_INPUT":
      case "signature-kit.RESPONSE_SHAPE":
      case "signature-kit.UNSUPPORTED_OPERATION":
        return Effect.fail(error.message)
      default:
        return Effect.fail(error.message)
    }
  }),
)

The formats and signers are separate packages — install only what each path uses.

npm install @signature-kit/xml @signature-kit/pdf @signature-kit/http @signature-kit/clicksign

How is this guide?

On this page