DEV Community

Cover image for Fixing yt-dlp Errors When Your JS Interpreter Gets Deprecated
Alan West
Alan West

Posted on

Fixing yt-dlp Errors When Your JS Interpreter Gets Deprecated

The 3 AM Cron Job That Broke

Last Tuesday I got paged because a personal archival script failed silently for two weeks. The script wraps yt-dlp to pull down conference talks I want to watch on flights. It had been chugging along fine for months, then suddenly: empty output, non-zero exit, no obvious error in the log.

If you maintain anything that shells out to a video extractor — archival tools, transcription pipelines, accessibility services — you've probably hit some flavor of this. A runtime gets deprecated upstream, your wrapper doesn't notice, and your pipeline rots. According to the upstream issue tracker, Bun is the latest JavaScript interpreter to be flagged as limited/deprecated for use with yt-dlp's JS-dependent extractors. The changelog mentions this is mostly about maintenance cost and compatibility gaps, not a permanent ban.

Let's walk through why this happens, how to debug it when your wrapper starts misbehaving, and what to switch to.

Why Tools Like yt-dlp Even Need a JS Runtime

If you've only used yt-dlp from the command line, you might not know it shells out to a JavaScript interpreter for certain sites. YouTube in particular obfuscates parts of its player code, and the only sane way to derive the real media URLs is to actually execute the player's JS and read the result.

The project supports multiple interpreters as fallbacks. From their docs, you can use Deno, Node, or other engines depending on what's installed. Bun was reportedly in that mix but is being de-prioritized — the issue thread mentions inconsistent behavior versus V8-based engines and the cost of maintaining a separate code path.

Here's the catch: yt-dlp will silently pick whatever interpreter it finds first on PATH. That's how you end up with a working script one day and a broken one the next, just because you brew installed something that put Bun ahead of Node.

Debugging Step One: Make It Loud

The first thing I do when a wrapper script goes quiet is force yt-dlp to actually tell me what it's doing. The -v flag dumps the entire decision tree, including which interpreter it picked.

# Run with verbose logging to see runtime selection
yt-dlp -v 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' 2>&1 | head -60
Enter fullscreen mode Exit fullscreen mode

Look for lines mentioning jsinterp, Deno, Node, or Bun. If you see Bun being selected and the extractor then bails on a signature decryption step, you've found your culprit. On my machine it looked roughly like this (paraphrased — the exact wording shifts between versions):

[debug] Using JS interpreter: Bun 1.x
[debug] Extracting signature function
ERROR: [youtube] Unable to extract nsig function code
Enter fullscreen mode Exit fullscreen mode

That second error is the giveaway. nsig is the obfuscated function YouTube uses, and certain interpreter quirks (different eval semantics, different Function constructor behavior) cause the extraction to fail without a clean stack trace.

The Fix: Pin a Known-Good Interpreter

The cleanest fix is to stop relying on PATH order and explicitly tell yt-dlp which engine to use. There's no single magic flag for this — the project picks via lookup — so the practical move is to make sure only the engines you trust are reachable.

For a one-off invocation, prepend a PATH that excludes Bun:

# Force a clean PATH that contains Node but not Bun
PATH="/usr/local/bin:/usr/bin:/bin" yt-dlp 'https://www.youtube.com/watch?v=...'
Enter fullscreen mode Exit fullscreen mode

For anything long-lived, I'd put it in a wrapper script so future-you doesn't have to remember:

#!/usr/bin/env bash
set -euo pipefail

# Strip Bun from PATH for this script's lifetime.
# We don't uninstall Bun globally because other projects depend on it.
CLEAN_PATH=$(echo "$PATH" | tr ':' '\n' | grep -v 'bun' | paste -sd ':' -)

export PATH="$CLEAN_PATH"

# Sanity check: confirm Node is reachable
command -v node >/dev/null || {
  echo "node not found — install it or add Deno" >&2
  exit 1
}

exec yt-dlp "$@"
Enter fullscreen mode Exit fullscreen mode

If you'd rather lean on Deno (which the project has supported for a while), install it via the official Deno install script and confirm it shows up in --verbose output as the selected interpreter.

Updating yt-dlp Itself

Half of the bug reports I've seen on this are downstream of running an outdated yt-dlp. The project ships fixes for site changes constantly — sometimes multiple times a week. If you installed via your distro's package manager six months ago, you're almost certainly behind.

# If installed via pip
python3 -m pip install --upgrade --user yt-dlp

# If you grabbed the standalone binary
yt-dlp -U
Enter fullscreen mode Exit fullscreen mode

For production-ish pipelines, I now pin a specific release and update on a schedule rather than chasing latest. It's a tradeoff: you trade "automatically gets the new fix" for "doesn't break at 3 AM because an extractor was rewritten."

Preventing the Next Silent Failure

A few habits I've picked up after getting burned by exactly this kind of issue:

  • Don't trust silent success. Check exit codes and output size. A 0-byte download with a zero exit is a failure.
  • Log the interpreter version. Capture yt-dlp --version and node --version (or whatever you're using) in your job logs. When something breaks two weeks later, you'll know if a runtime upgrade caused it.
  • Subscribe to the issue tracker. The yt-dlp GitHub issues are the canonical place to find out a runtime is being deprecated. Watching the repo or the releases page costs nothing.
  • Run a canary. A tiny job that downloads one known-stable video (your own test upload, ideally) every hour will surface breakage long before your real workload does.

Here's the canary I added after the incident:

#!/usr/bin/env bash
# canary.sh — fail loudly if yt-dlp can't pull a known short clip.
set -euo pipefail

TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT

yt-dlp -q -o "$TMP/%(id)s.%(ext)s" "$CANARY_URL"

# Verify we actually got bytes, not an empty file
SIZE=$(find "$TMP" -type f -printf '%s' 2>/dev/null || stat -f '%z' "$TMP"/* )
if [ "${SIZE:-0}" -lt 1024 ]; then
  echo "canary failed: output too small" >&2
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

That script is now wired into my monitoring. If it fails twice in a row, I get a notification — which is roughly how I should have set things up before this whole episode.

The Boring Takeaway

Most "my tool broke" mysteries come down to one of three things: an upstream change you didn't notice, an implicit dependency that shifted, or a silent error that didn't bubble up. The Bun-as-JS-interpreter situation is a textbook example of the second one. Make your dependencies explicit, log enough to debug, and add a canary so you find out from a monitor instead of from a user complaint two weeks later.

I haven't tested every interpreter combination thoroughly, but Node and Deno have been rock-solid for me across half a dozen archival pipelines. If you're picking one today, that's where I'd start.

Top comments (0)