SvelteKit 3 Is in Release Candidate. $lib Is Now #lib, Config Moves to vite.config.ts, and a Codemod Handles Most of the Rest.
SvelteKit 3 went to Release Candidate on 13 August, and the team's framing is that a stable release follows with no further breaking changes once people have kicked the tyres. There is a codemod that does most of the work in one command. The changes it cannot do for you are the ones worth knowing about before you start.
Up front: I do not ship SvelteKit day to day. This site runs on Astro. So read this as a careful pass through the migration notes with an eye for what bites, not as a war story from a production upgrade. Where I am inferring rather than reporting, I say so.
Run the codemod first, read the TODO list second
npx sv@next migrate sveltekit-3 --tasks all --confirm
That handles as much as can be handled mechanically and writes out a TODO list for the rest. For a new app it is npx sv@next create my-new-app. SvelteKit also emits diagnostic warnings and errors at runtime when it finds code that has not been updated, which is a nicer failure mode than silent behaviour changes.
Do this on a branch. The RC is explicitly a request for feedback, and bugs you find now can still change the stable release. That window closes.
The two changes most likely to eat an afternoon
Configuration moves out of svelte.config.js and into vite.config.ts. The reasoning given is that the Vite plugin benefits from having the config immediately rather than waiting on an async resolution step that cannot start until the whole Vite config resolves, which matters for tools like Vitest that may run from a directory that is not the project root. Fair enough. The practical consequence is that anything clever you were doing in svelte.config.js, and anything that reads it, now needs a home.
$lib becomes #lib. This is the one I would budget real time for. SvelteKit is dropping its own alias in favour of Node subpath imports, which Vite and TypeScript support natively, letting the framework delete the code that used to keep those two in sync. That is a genuinely good cleanup.
The gotcha is in the details. Node and TypeScript require subpath imports to be unambiguous, so #lib/foo is not valid. You need #lib/foo.ts or #lib/foo/index.ts. If your codebase leans on extensionless imports, and most do, that is a mechanical change across every import in the project. The codemod should cover the bulk of it, but this is the change I would grep for afterwards rather than trust.
TypeScript config gets shorter
Your tsconfig.json used to extend ./.svelte-kit/tsconfig.json. Now it extends $app/tsconfig, generated into node_modules/$app. It carries more recommended compiler options than the old one, and no longer needs the $lib path mapping, so you can probably delete most of your own compilerOptions unless you are doing something unusual.
Two things you now have to be explicit about: specify your include and exclude arrays yourself, and put your service worker in exclude.
Service workers stop being a special case
The old $service-worker module is gone. The things it exposed now come from ordinary places: $app/env, $app/paths, and a new $app/manifest module that gives you immutable, assets, prerendered and routes. You can import self from $app/service-worker to get correct typings for fetch events, provided you add a tsconfig.json next to the service worker extending $app/tsconfig/service-worker.
This is the change I like most on principle. A framework having a bespoke module that exists only inside one file is exactly the kind of special case that costs you every time you forget it exists.
Error handling actually changed behaviour
SvelteKit 2 supported Svelte 4, which had no error boundaries, so +error.svelte could only catch errors thrown during load, not during render. SvelteKit 3 requires Svelte 5 and covers both.
Two more that will change what you see in production. Every error now routes through your handleError hook, including ones you threw deliberately with error(...), which used to be skipped on the assumption you had already dealt with them. And sourcemaps are applied to stack traces, with the caveat that adapters need to catch up before that shows up everywhere in production.
If you have logging or error-reporting logic in handleError that assumed it only saw unexpected failures, check it before you upgrade. Deliberate 404s arriving in your error tracker is an annoying way to discover this.
Smaller items that still break things
- Shallow routing moves from
pushStateandreplaceStatetogotowith ashallow: trueoption. Shallow navigations now firebeforeNavigateand friends, andpersistState: truekeeps page state across a reload. - Redirecting to an external URL now requires an explicit
externaloption, eithertruefor any external URL or an array of allowed origins.javascript:URLs stay blocked either way. This is a security-shaped default change and I think it is the right call. - The
offvalue fordata-sveltekit-*link attributes is gone, replaced byfalse. - Clicking a link pointing at the current location now triggers a refresh rather than doing nothing.
- Explicit environment variables have left the experimental namespace. You declare what your app needs in
src/env.ts, mark each one public or private and build-time or boot-time, and can validate them with any Standard Schema library. Build-time resolution is what makes dead code elimination possible.
Vite 8 is a hard requirement, not a suggestion
SvelteKit 2 supported Vite 8. SvelteKit 3 requires it, which means Rolldown and faster builds, and it means the upgrade is gated on every Vite plugin you depend on supporting Vite 8. For an app with a thin plugin list this is nothing. For an app with a long tail of build-time plugins, this is the item that decides your timeline, and no codemod helps.
The team also adopted the Vite Environment API but deliberately did not implement FetchableDevEnvironment, saying it pushes too much complexity onto frameworks. The practical casualty is convenient access to Cloudflare Workers bindings in local dev, which they say they are working on by other means. If your local development depends on that, wait.
What I would actually do
Branch, run the codemod, and read the generated TODO list before touching anything by hand. Then check the three things the codemod cannot reason about: whether every Vite plugin you use supports Vite 8, whether your handleError can cope with deliberate errors arriving, and whether your extensionless #lib imports all got extensions.
Do not build anything on remote functions yet. The team is enthusiastic about them, says they make load and actions look clunky by comparison, and has still kept them behind an experimental flag. When maintainers are that keen on a feature and still will not stabilise it, believe the flag, not the enthusiasm.
The honest counter-take
The case against upgrading soon is decent. This is a release candidate, so the "no further breaking changes" promise is a stated intention rather than a guarantee. If you have a working SvelteKit 2 app with no pressing need for Svelte 5 error boundaries or Rolldown build times, waiting for stable costs you nothing except a slightly larger diff later.
The case for doing it now is narrower than "you should upgrade." It is that RC is the only period when your bug report can still change the release, and a framework upgrade you have already rehearsed on a branch is much less frightening than one you meet cold six months in. That is worth an afternoon, not a sprint.
Author
Lukas
@lukcombinator