Skip to content

Errors ​

Every error this library throws extends QuantumSafeError and carries three things you can rely on:

  • code: a stable machine-readable string such as QS_DECRYPTION_FAILED, safe to switch on;
  • hint: a one-line static suggestion for the usual fix (never contains key material, plaintext or your input);
  • toJSON(): a JSON-safe description for logs.

Match with instanceof or code. Never parse message. Messages may change between versions; codes and class names will not.

Handling errors ​

ts
import { easy, QuantumSafeError, DecryptionAuthenticationError } from 'quantum-safe-ts';

const a = easy.generateEncryptionKeys();
const b = easy.generateEncryptionKeys();
const sealed = easy.encrypt(a.publicKey, 'x');

try {
  easy.decrypt(b.secretKey, sealed); // wrong key
  throw new Error('should have failed');
} catch (e) {
  if (!(e instanceof DecryptionAuthenticationError)) throw e;
  if (!QuantumSafeError.is(e) || e.code !== 'QS_DECRYPTION_FAILED' || e.hint.length === 0) throw new Error('error contract');
  console.log(JSON.stringify(e.toJSON()));
}

QuantumSafeError.is(e) works across copies of the library. Prefer it to instanceof if your application might load both the ESM and the CommonJS build (each has its own classes, see runtimes).

A typical service wraps untrusted input like this:

ts
import { easy, QuantumSafeError } from 'quantum-safe-ts';

function openInbound(secretKey: string, bytes: Uint8Array): { ok: true; text: string } | { ok: false; reason: string } {
  try {
    return { ok: true, text: easy.decryptText(secretKey, bytes, { aad: 'inbox-v1' }) };
  } catch (e) {
    if (!QuantumSafeError.is(e)) throw e;                       // a bug of ours, not bad input: let it surface
    switch (e.code) {
      case 'QS_DECRYPTION_FAILED':                              // wrong key, wrong AAD, or modified data
      case 'QS_MALFORMED_CIPHERTEXT':
      case 'QS_KEY_PARSE_ERROR':
        return { ok: false, reason: e.code };                   // do not tell the sender more than the code
      default:
        throw e;
    }
  }
}

const keys = easy.generateEncryptionKeys();
const good = openInbound(keys.secretKey, easy.encrypt(keys.publicKey, 'hi', { aad: 'inbox-v1' }));
const bad = openInbound(keys.secretKey, new Uint8Array([1, 2, 3]));
if (!good.ok || bad.ok) throw new Error('unexpected');

Reference ​

ClasscodeThrown whenUsual fix
NotInitializedErrorQS_NOT_INITIALIZEDAn API was used before the WebAssembly was loadedawait init() once (outside Node.js)
DecryptionAuthenticationErrorQS_DECRYPTION_FAILEDWrong key, wrong AAD, or the message was modifiedCheck the key and the expectedAad; the data may be corrupt
VerificationErrorQS_VERIFICATION_FAILEDA signature, message, context, key or token claim does not match. No detail by design.Verify with the signer's public key and the same context
DecapsulationErrorQS_DECAPSULATION_FAILEDThe classical part of a KEM ciphertext is invalidThe ciphertext was not made for this key, or is corrupt
SigningErrorQS_SIGNING_FAILEDSigning could not completeCheck the secret key is intact and matches the algorithm
MalformedKeyErrorQS_MALFORMED_KEYA secret key has the wrong length or structureRe-export the key; check it matches the algorithm
MalformedCiphertextErrorQS_MALFORMED_CIPHERTEXTA ciphertext, sealed message or stream frame is structurally invalidThe data is corrupt or for another algorithm
MalformedSignatureErrorQS_MALFORMED_SIGNATURESignature bytes are structurally invalidThe data is corrupt
AlgorithmMismatchErrorQS_ALGORITHM_MISMATCHA key of one algorithm was used with another algorithm's APIUse the class that matches the key
HkdfOutputTooLongErrorQS_HKDF_OUTPUT_TOO_LONGderiveKey asked for more than 8,160 bytesAsk for less
KdfErrorQS_KDF_FAILEDArgon2id could not run (for example a salt under 8 bytes)Fix the parameters
KeyParseErrorQS_KEY_PARSE_ERRORCBOR, PEM or JWK key input is invalid: wrong type (pub / sec), wrong length for the algorithm, missing field, bad base64Check the source of the key
IncompatibleKeyVersionErrorQS_INCOMPATIBLE_KEY_VERSIONThe key was written by a newer format versionUpgrade this library
PayloadTooLargeErrorQS_PAYLOAD_TOO_LARGEA key, signed message or sealed message exceeds 10 MBThe input is not what you think it is
UnsupportedFormatErrorQS_UNSUPPORTED_FORMATA format that is not supported (for example a JWK for a secret key)Use CBOR or PEM for secret keys
UnsupportedAlgorithmErrorQS_UNSUPPORTED_ALGORITHMA name that is not a supported suite, or a suite not valid for that operationkemSuites() / sigSuites() list valid names
InvalidArgumentErrorQS_INVALID_ARGUMENTA wrong type or value, use after free(), a context over 255 bytes, a bad chunk sizeFix the call
PolicyViolationErrorQS_POLICY_VIOLATIONcnsa2.enforce rejected a configurationChoose the CNSA 2.0 parameter sets
CryptoError (base) / QuantumSafeErrorQS_INTERNAL_ERROR / QS_ERRORSomething unexpected: a bugReport it, without secret data

Class tree: QuantumSafeError has the subclasses NotInitializedError, CryptoError, SerializationError, UnsupportedAlgorithmError, InvalidArgumentError and PolicyViolationError. CryptoError covers the cryptographic failures; SerializationError covers KeyParseError, IncompatibleKeyVersionError, PayloadTooLargeError and UnsupportedFormatError.

Producing each common error on purpose ​

Useful for tests of your own error handling.

ts
import { HybridKEM, KEM, PublicKey, SecretBytes, Sign, cnsa2, utf8 } from 'quantum-safe-ts';
import type { QsErrorCode } from 'quantum-safe-ts';

function codeOf(fn: () => unknown): QsErrorCode | 'no error' {
  try {
    fn();
    return 'no error';
  } catch (e) {
    return (e as { code: QsErrorCode }).code;
  }
}

using pair = new HybridKEM().generateKeyPair();
const seen: Record<string, QsErrorCode | 'no error'> = {
  unknownSuite: codeOf(() => new Sign('NOT-A-SUITE' as never)),
  badPem: codeOf(() => PublicKey.fromPem('not a key')),
  wrongLengthPublicKey: codeOf(() => PublicKey.fromBytes('X25519+ML-KEM-768', new Uint8Array(10))),
  tooLarge: codeOf(() => PublicKey.fromCbor(new Uint8Array(10 * 1024 * 1024 + 1))),
  badCiphertext: codeOf(() => new HybridKEM().decapsulate(pair.secretKey, new Uint8Array(3))),
  algorithmMismatch: codeOf(() => new KEM('ML-KEM-768').decapsulate(pair.secretKey, new Uint8Array(1088))),
  hkdfTooLong: codeOf(() => SecretBytes.from(new Uint8Array(32)).deriveKey(8161, utf8('x'))),
  contextTooLong: codeOf(() => {
    const s = new Sign('ML-DSA-44');
    using k = s.generateKeyPair();
    s.sign(utf8('m'), k.secretKey, { context: new Uint8Array(256) });
  }),
  policy: codeOf(() => cnsa2.enforce({ kem: 'ML-KEM-768' })),
};
console.table(seen);
const expected: Record<string, QsErrorCode> = {
  unknownSuite: 'QS_UNSUPPORTED_ALGORITHM',
  badPem: 'QS_KEY_PARSE_ERROR',
  wrongLengthPublicKey: 'QS_KEY_PARSE_ERROR',
  tooLarge: 'QS_PAYLOAD_TOO_LARGE',
  badCiphertext: 'QS_MALFORMED_CIPHERTEXT',
  algorithmMismatch: 'QS_ALGORITHM_MISMATCH',
  hkdfTooLong: 'QS_HKDF_OUTPUT_TOO_LONG',
  contextTooLong: 'QS_INVALID_ARGUMENT',
  policy: 'QS_POLICY_VIOLATION',
};
for (const [name, code] of Object.entries(expected)) if (seen[name] !== code) throw new Error(`${name}: expected ${code}, got ${seen[name]}`);

Errors you will not see ​

  • ML-KEM never throws for a wrong ciphertext (implicit rejection, FIPS 203): decapsulation returns a pseudo-random secret and the failure appears later as DecryptionAuthenticationError from Envelope.open.
  • Signature failures give no reason. VerificationError does not say whether the message, signature, context or key was wrong.
  • Hints never contain your data. They are static strings, so they are safe to show to users and to put in logs.

For coding agents: the MCP server has an explain_error tool that looks up a code or class name.

Apache-2.0.