The error, verbatim
Jump to the fix ↓[vite] Internal server error: Failed to resolve import "@/utils/hello"
from "src/main.js". Does the file exist?
Error: [vite]: Rolldown failed to resolve import "@/utils/hello" from ".../src/main.js"
Tested on
- Vite
- 8.3.5 and 8.3.0 (Rolldown); Vitest 5.0.5
- Plugin
- vite-tsconfig-paths 6.1.1
- Node
- 24.14.0
- OS
- Windows 11 Pro; Ubuntu on GitHub Actions
✓ Last re-run on GitHub Actions · see the run · all repros
Contents
@/ isn’t a real path. It’s an alias: a shortcut that something in your toolchain has to translate into src/. When nothing does, Vite stops with this in the terminal, and as a red overlay in the browser:
[vite] Internal server error: Failed to resolve import "@/utils/hello" from "src/main.js". Does the file exist?
Plugin: vite:import-analysis
The confusing part is that the same project can build fine and still fail in the dev server. I tested every common setup on Vite 8.3.5, with vite build, the dev server (vite) and Vitest, on Windows and on Linux:
| Setup | vite build |
dev server | Vitest |
|---|---|---|---|
Only paths in tsconfig.json, no alias |
✅ built if the tsconfig covers the file (allowJs for .js), ❌ otherwise |
❌ HTTP 500, the error above | ❌ Cannot find package '@/utils/hello' |
resolve.alias with fileURLToPath |
✅ | ✅ | ✅ |
resolve.alias set to "/src" |
✅ | ✅ | ✅ |
resolve.tsconfigPaths: true |
✅ | ✅ | ✅ |
vite-tsconfig-paths plugin |
✅ | ✅ | ✅ |
resolve.alias set to "./src" (relative string) |
❌ os error 2 |
The fix
Define the alias in vite.config.js (or .ts), so the build, the dev server and Vitest all use it:
import { defineConfig } from "vite";
import { fileURLToPath } from "node:url";
export default defineConfig({
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});If you already keep paths in tsconfig.json, you can tell Vite to read them instead of repeating the alias:
export default defineConfig({
resolve: { tsconfigPaths: true },
});Keep the matching paths entry in tsconfig.json either way. That’s what your editor and the TypeScript type checker read; Vite’s alias doesn’t reach them.
Why the build passes but the dev server fails
With only "paths": { "@/*": ["src/*"] } in tsconfig.json (plus "allowJs": true, since my files are .js) and no alias, vite build on 8.3.5 finished (✓ 5 modules transformed, ✓ built), and the bundle contained the imported code. The same result on 8.3.0. Without allowJs, or without tsconfig.json, the same build failed with Rolldown failed to resolve import "@/utils/hello". So Vite 8’s production build picks up tsconfig paths for the files that tsconfig covers, which is why one project’s build passes and another’s, with the same paths, doesn’t.
The dev server didn’t. It printed a warning at startup:
(!) Failed to run dependency scan. Skipping dependency pre-bundling. Error: The following dependencies are imported but could not be resolved:
@/utils/hello (imported by .../src/main.js)
Are they installed?
and then answered the browser’s request for src/main.js with HTTP 500 and the “Failed to resolve import” error. Vitest, which runs through Vite’s dev pipeline, failed the same way. The fix above makes all three agree. Don’t rely on a green build: CI that only runs vite build won’t catch it.
What didn’t work
The array form also works, if you need several aliases or a regex:
import path from "node:path";
export default { resolve: { alias: [{ find: "@", replacement: path.resolve(import.meta.dirname, "src") }] } };
It works on Windows but fails on Linux CI: letter case
A file named Hello.js, imported as @/utils/hello, built without complaint on Windows. On Linux the same build failed:
[UNLOADABLE_DEPENDENCY] Could not load src/utils/hello
Windows file names ignore case and Linux’s don’t, so the alias resolves to a file Linux says doesn’t exist. Make the import match the file name exactly. If your CI runs on Linux and you develop on Windows or macOS, this is the first thing to check when an alias “suddenly” breaks only in CI.
Same error, different wording
- Dev server:
Failed to resolve import "…" from "…". Does the file exist?, in the terminal and as a red overlay in the browser. - Build in Vite 8:
Rolldown failed to resolve import. Vite 8 bundles with Rolldown, so older answers that say “Rollup failed to resolve import” are describing the same problem. - A typo in a relative path (
./utils/helo) gives a different build message:[UNRESOLVED_IMPORT] Could not resolve './utils/helo'. Check the spelling and extension.
How this was tested
October 31, 2026. A small project (src/main.js importing @/utils/hello, an index.html, a Vitest test importing from @/, and a tsconfig.json with "paths": { "@/*": ["src/*"] }) on Vite 8.3.5 and 8.3.0, Vitest 5.0.5 and Node 24.14.0, on Windows 11 Pro and on GitHub Actions ubuntu-latest and windows-latest. Each config was built with vite build; the dev server was started and src/main.js requested over HTTP; Vitest ran the same test. The original version of this post (September 22) tested Vite 8.3.0 with a .js project and no allowJs, which is why tsconfig paths alone failed its build. It also called resolve.tsconfigPaths “not a Vite option”; that was wrong, and it’s corrected above. The scripts and the full output are public.
— N.K., end of entry No.009