Error message
OperationError: The operation failed for an operation-specific reason
WebCrypto crypto.subtle — browsers & Node
What it means
The deliberately vague catch-all of WebCrypto: the primitive itself failed. For decrypt() with AES-GCM it means the authentication tag did not verify — wrong key, wrong IV, wrong additional data, or modified ciphertext. The spec keeps the message generic so implementations do not leak why a cryptographic check failed. For sign() and deriveBits() the same name covers internal primitive failures, so the operation in your stack trace matters more than the message text.
Why it happens
AES-GCM authentication failure
commonDecrypting with a different key than encrypted, a mismatched IV, missing/different additionalData, or ciphertext that lost its appended 16-byte tag in transport. GCM is authenticated: any of these fails the whole operation rather than returning garbage.
Ciphertext truncated or re-encoded
commonBase64 handled inconsistently (URL-safe vs standard alphabet), a database column cutting bytes, or string/binary conversions corrupting the buffer. The tag check catches every such change — that is its job.
Derivation parameters out of range
occasionalderiveBits/deriveKey with an unsatisfiable length, PBKDF2 with zero iterations, or an ECDH computation on inconsistent inputs surfaces as OperationError in several runtimes.
How to fix it
- 1.
Ship IV and ciphertext together, decode symmetrically
Prepend the IV to the ciphertext and split on decrypt, so both sides always agree; use one base64 flavor everywhere.
js // encrypt const iv = crypto.getRandomValues(new Uint8Array(12)); const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data); const packed = new Uint8Array([...iv, ...new Uint8Array(ct)]); // decrypt const buf = packedFromWire; const pt = await crypto.subtle.decrypt( { name: "AES-GCM", iv: buf.slice(0, 12) }, key, buf.slice(12), ); - 2.
Treat it as an integrity verdict, not corruption
When decrypt throws OperationError, the correct response is "this ciphertext is not authentic under this key" — reject it. Do not retry with padding tweaks or fall back to unauthenticated modes; the check failing is the security feature.
- 3.
Compare key fingerprints across the two sides
Export both keys and hash them; differing digests end the mystery in one step.
js const raw = await crypto.subtle.exportKey("raw", key); const fp = await crypto.subtle.digest("SHA-256", raw); console.log(Buffer.from(fp).toString("hex").slice(0, 16)); // compare on both sides