Fixed: "Uncaught SyntaxError: Unexpected token '<'" Loading Vite Assets

The problem: The browser console throws Uncaught SyntaxError: Unexpected token '<' pointing at a compiled JS file, and the page loads with zero interactivity.

Environment: Laravel 11, Vite 5, Nginx/Apache production server, deployed via Forge/manual deploy

The Error

● ● ●  console
Uncaught SyntaxError: Unexpected token '<' (at app-4f9c2a1b.js:1:1)

GET https://example.com/build/assets/app-4f9c2a1b.js 404 (Not Found)

Why It Happens

This error means the browser asked for a JavaScript file, but got back an HTML page instead — usually your server's 404 page, or in some setups a fallback index.html. The browser tries to parse that HTML as JS, hits the opening < of <!DOCTYPE html>, and throws immediately.

The root cause is almost always a mismatch between the hashed filename Laravel's Blade template expects and what actually exists in public/build. This happens when:

1. You ran npm run build locally or in CI, but the deployment didn't actually copy the new public/build directory to the server — so Blade's @vite tag references a hash from your last build, but the server still has an older (or no) build folder.

2. Laravel's view cache has a compiled Blade template baked in referencing an old manifest hash, even though the actual manifest.json was updated.

3. Your web server's rewrite rules are catching the /build/assets/*.js request and routing it through Laravel's catch-all route (common with SPA-style fallback rules), which then returns your 404 or welcome page as HTML instead of serving the static file directly.

The Fix

First, confirm the build actually exists on the server and matches what Blade expects:

ls public/build/assets/
cat public/build/manifest.json | grep "app-"

If the file listed in manifest.json doesn't match what's actually in assets/, rebuild and clear all caches:

npm run build
php artisan view:clear
php artisan cache:clear
php artisan config:clear

If the build files exist and are correct but the server still returns HTML, check your Nginx config for a catch-all rule that's too greedy and swallowing static asset requests:

/etc/nginx/sites-available/app.conf
location /build/ {
    try_files $uri =404;
}

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

This tells Nginx to serve /build/ assets directly and return a real 404 if missing, rather than falling through to Laravel's router — which is what silently returns HTML.

Step-by-Step

  1. Open the failing asset URL directly in a new browser tab and check whether it returns JS content or an HTML error page.
  2. SSH into the server and confirm public/build/manifest.json matches the hash Blade is requesting.
  3. If mismatched, run npm run build on the server (or re-deploy your CI build artifact) and clear Laravel's view/config/cache.
  4. If the file exists and hash matches but still 404s in-browser, inspect your Nginx/Apache config for a catch-all rewrite rule.
  5. Add an explicit location /build/ block (Nginx) or equivalent <Directory> rule (Apache) so static assets bypass Laravel's router entirely.
  6. Reload the web server config (sudo nginx -s reload) and hard-refresh the page to confirm the JS now loads with a 200 status.

Edge case: If you're using Laravel Forge's zero-downtime deployment, the symlinked current release folder can briefly point to an old release mid-deploy while public/build is only half-updated — if this error appears only for a few seconds right after deploy and then resolves itself, it's a deploy-timing race condition, not a config bug. Adding a build-verification step before the symlink swap in your deploy script fixes this permanently.

Still getting HTML instead of JS after checking all this? Reach out on the Contact page and I'll help trace your specific setup.

Advertisement
Advertisement