denoland/deno · error · TypeError

If `cert` is specified, `key` must be specified as well for

Error message

If `cert` is specified, `key` must be specified as well for `${api}`

What it means

loadTlsKeyPair requires cert and key to be passed together; specifying cert without key throws this TypeError, with the message naming the offending API (Deno.connectTls or Deno.listenTls). A TLS identity is inherently a pair - a certificate alone cannot be used, so the check runs before any socket or TLS context is created.

Solutions

  1. Pass both strings: Deno.listenTls({ port, cert: await Deno.readTextFile('cert.pem'), key: await Deno.readTextFile('key.pem') })
  2. Verify both files/env vars exist before building the options object
  3. If you do not need a client certificate, omit cert as well - server-only verification needs neither

Example fix

// before
Deno.listenTls({ port: 443, cert }); // key missing

// after
const [cert, key] = await Promise.all([
  Deno.readTextFile('cert.pem'),
  Deno.readTextFile('key.pem'),
]);
Deno.listenTls({ port: 443, cert, key });
Defensive patterns

Strategy: validation

Validate before calling

function requireKeyPair(opts: { cert?: string; key?: string }): { cert: string; key: string } {
  if (opts.cert === undefined || opts.key === undefined) {
    throw new Error('TLS identity requires both cert and key');
  }
  return { cert: opts.cert, key: opts.key };
}

Type guard

function hasCompleteKeyPair(o: { cert?: string; key?: string }): o is { cert: string; key: string } {
  return typeof o.cert === 'string' && typeof o.key === 'string' &&
    o.cert.length > 0 && o.key.length > 0;
}

Try / catch

try {
  Deno.listenTls(opts);
} catch (e) {
  if (e instanceof TypeError && e.message.includes('`key` must be specified')) {
    throw new Error(`missing TLS key file (cert was loaded from ${certPath})`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Deno.listenTls({ port: 443, cert: pem }) with no key; Deno.connectTls({ cert }) attempting mutual TLS with a missing client key; destructuring a config object where the key field was dropped (typo, undefined).

Common situations: Secrets split across files where only cert.pem got deployed; config objects assembled conditionally so key ends up undefined; examples copied from docs that omitted the key line.

Related errors


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

Appendix: source

Thrown at ext/net/02_tls.js:148

function loadTlsKeyPair(api, {
  keyFormat,
  cert,
  key,
}) {
  // TODO(mmastrac): remove this temporary symbol when the API lands
  if (arguments[1][resolverSymbol] !== undefined) {
    return createTlsKeyResolver(arguments[1][resolverSymbol]);
  }

  // Check for "pem" format
  if (keyFormat !== undefined && keyFormat !== "pem") {
    throw new TypeError(
      `If "keyFormat" is specified, it must be "pem": received "${keyFormat}"`,
    );
  }

  if (cert !== undefined && key === undefined) {
    throw new TypeError(
      `If \`cert\` is specified, \`key\` must be specified as well for \`${api}\``,
    );
  }
  if (cert === undefined && key !== undefined) {
    throw new TypeError(
      `If \`key\` is specified, \`cert\` must be specified as well for \`${api}\``,
    );
  }

  if (cert !== undefined) {
    return op_tls_key_static(cert, key);
  } else {
    return op_tls_key_null();
  }
}

function listenTls({
  port = 0,

View on GitHub (pinned to 9ad36f7a2c)