denoland/deno · error · Error
ERR_CRYPTO_INVALID_STATE
ERR_CRYPTO_INVALID_STATE
Error message
Invalid state for operation final
What it means
Cipheriv is a one-shot state machine: final() flushes the last block, sets _finalized = true, and (at the native layer, op_node_cipheriv_final takes the context out of the resource table) consumes the context. A second final() call hits `if (this._finalized)` and throws ERR_CRYPTO_INVALID_STATE('final'). The same flag also blocks update()/setAAD() after final, but the 'final' operation name means final() itself ran twice.
Source
Thrown at ext/node/polyfills/internal/crypto/cipher.ts:262
this._needsBlockCache = !this._isAesWrap &&
!(cipher == "aes-128-gcm" || cipher == "aes-256-gcm" ||
cipher == "aes-128-ctr" || cipher == "aes-192-ctr" ||
cipher == "aes-256-ctr" || cipher == "chacha20" ||
cipher == "chacha20-poly1305");
this._authTag = undefined;
this._autoPadding = true;
this._finalized = false;
this._decoder = undefined;
}
ObjectSetPrototypeOf(Cipheriv.prototype, getTransform().prototype);
ObjectSetPrototypeOf(Cipheriv, getTransform());
Cipheriv.prototype.final = function (
encoding: string = getDefaultEncoding(),
): Buffer | string {
if (this._finalized) {
throw new ERR_CRYPTO_INVALID_STATE("final");
}
_lazyInitCipherDecoder(this, encoding);
if (this._isAesWrap) {
this._finalized = true;
return encoding === "buffer" ? Buffer.from([]) : this._decoder!.end();
}
const bs = this._blockSize;
const buf = new FastBuffer(bs);
const hasNoBufferedData =
TypedArrayPrototypeGetByteLength(this._cache.cache) === 0;
const shouldPadEmptyBlock = this._needsBlockCache && this._autoPadding;
if (hasNoBufferedData && !shouldPadEmptyBlock) {
const maybeTag = op_node_cipheriv_take(this._context);
if (maybeTag) this._authTag = Buffer.from(maybeTag);View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Call final() exactly once per cipher instance; create a fresh createCipheriv() for every message.
- Wrap the cipher so final() is idempotent (track a local boolean) or move final() out of finally blocks.
- Make ownership explicit: the function that creates the cipher is the only one that finalizes it.
Example fix
// before
function encrypt(msg) {
const c = crypto.createCipheriv('aes-256-gcm', key, iv);
c.update(msg);
helperFlush(c); // calls c.final()
return c.final(); // ERR_CRYPTO_INVALID_STATE('final')
}
// after
function encrypt(msg) {
const c = crypto.createCipheriv('aes-256-gcm', key, iv);
c.update(msg);
return c.final(); // single, owned by this function
} Defensive patterns
Strategy: validation
Validate before calling
function once<T extends (...args: any[]) => any>(fn: T): T {
let called = false;
return ((...args: any[]) => {
if (called) throw new Error('final() called twice');
called = true;
return fn(...args);
}) as T;
}
cipher.final = once(cipher.final.bind(cipher)); // idempotent-by-failure wrapper Try / catch
try { out = cipher.final(); } catch (e) { if (e.code === 'ERR_CRYPTO_INVALID_STATE' && e.message.includes('final')) { throw new LogicError('cipher already finalized — check double final()'); } throw e; } Prevention
- One creator, one finalizer: keep cipher construction and final() in the same function scope.
- Never put final() in a finally block that can follow an early return.
- Build ciphers per message; do not pool or cache them.
When it happens
Trigger: Calling final() inside a helper and again in the caller; a `finally { cipher.final() }` block that runs after an earlier return already finalized; reusing one cipher object across loop iterations or queued async jobs.
Common situations: Double-cleanup error paths; libraries that auto-finalize cipher streams they are handed; copy-pasted encrypt helpers where ownership of final() is unclear.
Related errors
- ERR_INVALID_ARG_TYPE
- ERR_CRYPTO_UNKNOWN_CIPHER
- Trying to add data in unsupported state
- ERR_UNKNOWN_ENCODING
- ERR_INVALID_ARG_VALUE
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/4fd43cc713d35eff.
Report an issue: GitHub.