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 18"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, more specific than the default.operation?— the operation where the failure occurred, useful for logging and telemetry.get message()— the human-readable message; resolves toreasonwhen present, otherwise to the code's default text.
import { Effect } from "effect"
import type { SignatureArtifact, SignatureKitError } from "@signature-kit/core/config"
// The error channel is typed: each failure is a SignatureKitError.
declare const sign: Effect.Effect<SignatureArtifact, SignatureKitError>
// _tag: "SignatureKitError"
class SignatureKitError {
readonly code: SignatureKitErrorCode // 18 "signature-kit.*" literals
readonly retryable: boolean // decided at the point of failure
readonly reason?: string // contextual message
readonly operation?: SignatureKitOperation
get message(): string // default per code (reason ?? default)
}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 18 literals.
import { Effect } from "effect"
import { a1SignaturesLayer } from "@signature-kit/a1/signer"
import { signatures } from "@signature-kit/core/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"
// 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 18 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.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.HTTPRemote signature HTTP request failed.· editablesignature-kit.RESPONSE_SHAPERemote signature response shape was invalid.· editablesignature-kit.UNSUPPORTED_OPERATIONRemote signature operation is unsupported.· editablesignature-kit.UNKNOWNUnknown SignatureKit failure.· editableFormat 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, a hashAlgorithm outside of sha256/sha512 fails as pdf.SIGN_FAILED.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/engine"
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 { createClicksignSignatureRequest } from "@signature-kit/clicksign"
import { signatureHttpClientLive } from "@signature-kit/core/http"
const request = createClicksignSignatureRequest(options, {
title: "Contract",
documents,
recipients,
}).pipe(
Effect.provide(signatureHttpClientLive),
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/core @signature-kit/clicksign