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 samecodemay arrive withretryable: trueat one call site andfalseat another.reason?— an optional contextual message for logs and telemetry. UI copy should key offcode, not parse this text.operation?— a plainstringnaming 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 fromSchema.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, orreasonfor codes marked editable.
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.
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:
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.
signature-kit.EMPTY_FILECertificate file is empty (0 bytes).signature-kit.INVALID_FORMATThe file is not a PKCS#12 (.pfx/.p12) certificate.· editablesignature-kit.INVALID_INPUTInvalid signing input.· editablesignature-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.· editablesignature-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.· editablesignature-kit.PEM_EXTRACTION_FAILEDFailed to extract PEM material from the PFX.signature-kit.KEY_IMPORT_FAILEDFailed to import the key into Web Crypto.· editablesignature-kit.DIGEST_FAILEDFailed to compute the certificate digest.signature-kit.SIGN_FAILEDFailed to sign the content.· editablesignature-kit.VERIFY_FAILEDFailed to verify the signature.· editablesignature-kit.HTTPHTTP request failed.· editablesignature-kit.RESPONSE_SHAPEHTTP response shape was invalid.· editablesignature-kit.UNSUPPORTED_OPERATIONOperation is unsupported.· editablesignature-kit.UNKNOWNUnknown SignatureKit failure.· editableLocalized display messages
Do not localize by sniffing classes, loose .code fields, or substrings from reason. That path breaks when the diagnostic text changes:
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:
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
SignatureKitError with provider, operation, schemaName, and status when that metadata exists.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:
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/clicksignHow is this guide?