Error handling
Handle errors in the @cipherstash/stack SDK with the Result pattern, covering error types, initialization failures, bulk operations, and identity issues.
All async methods in the SDK return a Result object — a discriminated union with either a data key (success) or a failure key (error). You never get both.
The one exception is Encryption() initialization, which throws on failure rather than returning a Result. See Handling initialization errors below.
The Result pattern
const result = await client.encrypt("[email protected]", {
column: users.email,
table: users,
})
if (result.failure) {
// result.failure.type: string (e.g. "EncryptionError")
// result.failure.message: string (human-readable description)
console.error(result.failure.type, result.failure.message)
} else {
// result.data: Encrypted payload
console.log(result.data)
}This pattern applies to every async method: encrypt, decrypt, encryptModel, decryptModel, bulkEncrypt, bulkDecrypt, bulkEncryptModels, bulkDecryptModels, encryptQuery, and LockContext.identify(). The one exception is Encryption() initialization, which throws on failure.
Error types
| Type | When it occurs | Common causes |
|---|---|---|
ClientInitError | Encryption() initialization | Missing or invalid credentials, unreachable ZeroKMS endpoint, no schemas provided |
EncryptionError | encrypt, encryptModel, bulkEncrypt, encryptQuery | Invalid plaintext for the column's data type, NaN/Infinity for numeric columns, non-string value for freeTextSearch column |
DecryptionError | decrypt, decryptModel, bulkDecrypt | Corrupted ciphertext, wrong keyset, lock context mismatch (data was encrypted with a different identity) |
LockContextError | LockContext operations | Invalid JWT, missing required claims, CTS endpoint unreachable |
CtsTokenError | CTS token exchange | JWT rejected by CipherStash Token Service, expired token, CTS endpoint misconfigured |
Handling initialization errors
Client initialization is the most common place to encounter errors. Unlike other SDK operations, Encryption() throws on failure rather than returning a Result.
import { Encryption } from "@cipherstash/stack"
import { users } from "./schema"
try {
const client = await Encryption({ schemas: [users] })
} catch (error) {
// Check your CS_* environment variables
console.error("Failed to initialize:", error.message)
process.exit(1)
}Common initialization issues
| Symptom | Cause | Fix |
|---|---|---|
"Missing workspace CRN" | CS_WORKSPACE_CRN not set | Set the env var or pass workspaceCrn in config |
"Missing client ID" | CS_CLIENT_ID not set | Set the env var or pass clientId in config |
"Missing client key" | CS_CLIENT_KEY not set | Set the env var or pass clientKey in config |
"Missing access key" | CS_CLIENT_ACCESS_KEY not set | Set the env var or pass accessKey in config |
| Connection timeout | ZeroKMS endpoint unreachable | Check network connectivity and firewall rules |
Handling encrypt/decrypt errors
const encrypted = await client.encrypt(value, {
column: users.email,
table: users,
})
if (encrypted.failure) {
switch (encrypted.failure.type) {
case "EncryptionError":
// Log the error and decide whether to retry or fail
console.error("Encryption failed:", encrypted.failure.message)
break
}
}Common encrypt/decrypt issues
| Symptom | Cause | Fix |
|---|---|---|
"NaN is not a valid value" | Passed NaN to a numeric column | Validate inputs before encrypting |
"Infinity is not a valid value" | Passed Infinity to a numeric column | Validate inputs before encrypting |
"Expected string value" | Non-string value for a column with .freeTextSearch() | Convert to string or remove the freeTextSearch index |
| Decryption fails with lock context error | Data was encrypted with a different identity | Ensure the same user's JWT is used for both encrypt and decrypt |
| Decryption returns corrupted data error | Wrong keyset or tampered ciphertext | Verify the keyset matches the one used during encryption |
Handling bulk operation errors
Bulk operations (bulkDecrypt, bulkDecryptModels) support per-item error handling. The overall operation succeeds but individual items may fail.
const result = await client.bulkDecrypt(encryptedPayloads)
if (result.failure) {
// Entire operation failed (e.g., ZeroKMS unreachable)
console.error("Bulk decrypt failed:", result.failure.message)
} else {
// Per-item results
for (const item of result.data) {
if ("data" in item) {
console.log(`${item.id}: decrypted successfully`)
} else {
// Individual item failed
console.error(`${item.id}: ${item.error}`)
}
}
}Handling identity errors
When using identity-aware encryption, errors can occur during JWT identification or when using lock contexts.
import { LockContext } from "@cipherstash/stack/identity"
const lc = new LockContext()
const identifyResult = await lc.identify(userJwt)
if (identifyResult.failure) {
switch (identifyResult.failure.type) {
case "LockContextError":
// JWT is invalid or missing required claims
console.error("Identity error:", identifyResult.failure.message)
break
case "CtsTokenError":
// Token exchange with CTS failed
console.error("CTS error:", identifyResult.failure.message)
break
}
}Common identity issues
| Symptom | Cause | Fix |
|---|---|---|
LockContextError with missing claim | JWT doesn't contain the configured identity claim | Check your JWT contains the sub claim (or your custom claims) |
CtsTokenError with connection error | CTS endpoint unreachable | Verify CS_CTS_ENDPOINT is set correctly |
CtsTokenError with rejection | JWT is expired or not trusted by CTS | Ensure JWT is fresh and from a configured identity provider |
Error type constants and utilities
The @cipherstash/stack/errors subpath exports runtime constants, the StackError union type, and a utility for safely extracting error messages.
StackError union type
StackError is a discriminated union of all error types, enabling exhaustive switch handling:
import { EncryptionErrorTypes, type StackError, getErrorMessage } from "@cipherstash/stack/errors"
function handleError(error: StackError) {
switch (error.type) {
case EncryptionErrorTypes.ClientInitError:
console.error("Init failed:", error.message)
break
case EncryptionErrorTypes.EncryptionError:
console.error("Encrypt failed:", error.message, error.code)
break
case EncryptionErrorTypes.DecryptionError:
console.error("Decrypt failed:", error.message)
break
case EncryptionErrorTypes.LockContextError:
console.error("Lock context failed:", error.message)
break
case EncryptionErrorTypes.CtsTokenError:
console.error("CTS token failed:", error.message)
break
default:
// TypeScript ensures exhaustiveness
const _exhaustive: never = error
}
}getErrorMessage
Use getErrorMessage to safely extract a message from any thrown value:
import { getErrorMessage } from "@cipherstash/stack/errors"
try {
await client.encrypt("data", { column: users.email, table: users })
} catch (e) {
console.error(getErrorMessage(e))
}Wrapping errors in application code
Create a helper that converts Result into thrown errors for frameworks that expect exceptions (e.g., Express, Next.js API routes):
function unwrap<T>(result: { data?: T; failure?: { type: string; message: string } }): T {
if (result.failure) {
throw new Error(`[${result.failure.type}] ${result.failure.message}`)
}
return result.data as T
}
// Usage
const encrypted = unwrap(await client.encrypt("hello", { column: users.email, table: users }))The Result pattern is intentionally designed to avoid thrown exceptions. Use the unwrap helper only at your application's boundaries where exceptions are expected.
Logging
Control SDK log verbosity with the STASH_STACK_LOG environment variable:
STASH_STACK_LOG=error # debug | info | error (default: error)| Value | What is logged |
|---|---|
error | Errors only (default) |
info | Info and errors |
debug | Debug, info, and errors |
The SDK never logs plaintext data or key material at any log level.
Drizzle adapter reference
Encrypted query operators, schema extraction, EQL migration generation, and API surface for @cipherstash/stack/drizzle.
Migration guide
Migrate from @cipherstash/protect to @cipherstash/stack with mapped imports, renamed functions, and updated schema definitions in a few simple steps.