denoland/deno · error
dev server exited without printing a URL
Error message
dev server exited without printing a URL
What it means
Thrown when a framework dev server spawned for `deno desktop --hmr` (the framework's hmr_command, e.g. Vite via npm) exits before printing a startup line that matches the `Local: <http(s)://...>` pattern scraped by parse_dev_server_url. The stdout lines are echoed while scanning, so the framework's own error output usually appears just above this message. A sibling error covers the 15s timeout case where the process stays alive but never prints a URL.
Solutions
- Scroll up and read the dev server's own output — it is echoed above this error and names the real failure.
- Run the framework's dev command manually (e.g. `npm run dev`) in the project directory and fix whatever crash it reports.
- Install dependencies (`npm install` / `pnpm install`) and clear the blocked port (`lsof -ti :5173 | xargs kill`).
- If using a custom hmr_command whose output differs from Vite's `Local:` line, switch to a Vite-based dev server or extend the detection upstream.
Example fix
# before deno desktop --hmr main.ts # fails: vite crashed on missing deps # after npm install && npm run dev # confirm the banner prints a "Local:" URL, then: deno desktop --hmr main.ts
Defensive patterns
Strategy: try-catch
Validate before calling
# bash: prove the dev server starts and prints a Local: URL before using --hmr
npm install
(npm run dev & DEV_PID=$!; sleep 5; kill $DEV_PID) 2>&1 | grep -q "Local:" \
|| { echo "dev server did not print a Local: URL" >&2; exit 1; } Try / catch
# In the wrapper script / CI, capture deno's output and branch on the failure
set +e
OUT="$(deno desktop --hmr main.ts 2>&1)"; RC=$?
set -e
if [[ $RC -ne 0 ]]; then
if grep -q "exited without printing a URL" <<<"$OUT"; then
echo "dev server failed to start — run 'npm run dev' to see the cause" >&2
fi
exit $RC
fi Prevention
- Always `npm install` before the first --hmr run on a fresh clone.
- Keep the framework's default dev script (Vite prints the `Local:` line this feature scrapes).
- Watch for and free occupied dev ports (5173 etc.) before launching.
- Run the dev command by hand whenever you change vite.config or the dev script.
When it happens
Trigger: The dev-server command fails immediately: missing node_modules (`npm install` not run), bad `dev` script, unsupported Node version, port already in use, or the framework crashed on startup. Also triggered when a non-Vite dev command prints its URL in a different format that the `Local:` regex does not match — but only if the process then exits; a live server that never matches hits the 15s timeout instead.
Common situations: Fresh clone where dependencies are not installed; `vite.config` error or syntax error in the entry; port 5173 occupied by a stale process; custom dev script (webpack-dev-server, `deno serve`) whose banner does not say `Local:`; CI without a TTY where the dev server refuses to start.
Related errors
- dev server with HMR did not start within 15s
- build failed (exit code: )
- failed to capture HMR dev server stdout
- icon ' ' must be .icns or .png
- icon ' ' not found
AI-assisted analysis of denoland/deno@f7822238ca (2026-08-20).
Data as JSON: /api/errors/68af13132e80f5cf.
Report an issue: GitHub.
Appendix: source
Thrown at cli/tools/desktop.rs:1226
format!("failed to spawn HMR dev server: {:?}", cmd_args)
})?;
let stdout = child.stdout.take().ok_or_else(|| {
deno_core::anyhow::anyhow!("failed to capture HMR dev server stdout")
})?;
let mut lines = BufReader::new(stdout).lines();
let url = tokio::time::timeout(std::time::Duration::from_secs(15), async {
while let Ok(Some(line)) = lines.next_line().await {
let url = parse_dev_server_url(&line);
// Echo the dev server's own startup output (banner, URL, warnings) so
// it isn't swallowed while we scan for the URL.
log::info!("{line}");
if let Some(url) = url {
return Ok(url);
}
}
deno_core::anyhow::bail!("dev server exited without printing a URL")
})
.await
.map_err(|_| {
deno_core::anyhow::anyhow!(
"{name} dev server with HMR did not start within 15s"
)
})??;
// Keep forwarding the dev server's stdout so its HMR/compile logs stay
// visible after startup instead of being silently swallowed. The task ends
// on its own once the child is killed (on drop) and the pipe closes.
tokio::spawn(async move {
while let Ok(Some(line)) = lines.next_line().await {
log::info!("{line}");
}
});
Ok((url, child))View on GitHub (pinned to f7822238ca)