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

  1. Call final() exactly once per cipher instance; create a fresh createCipheriv() for every message.
  2. Wrap the cipher so final() is idempotent (track a local boolean) or move final() out of finally blocks.
  3. 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

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


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/4fd43cc713d35eff. Report an issue: GitHub.