The SvelteKit 2 Migration Guide

| Published: January 23, 2024

| Updated: January 23, 2024

If you haven’t heard, SvelteKit 2 is out! The new version brings with it some unique new features, and also a few API changes that you’ll need to look at before migrating.

How it feels wanting to upgrade to SvelteKit 2

I released a SvelteKit 2 post discussing most of the major changes, which centered mostly around the shallow routing feature. As much as I love this feature, there’s plenty of apps that first need to move their codebase to the new version before they can think about implementing this in their app.

It’s important to keep up your apps up to date with the latest version in the interest of:

  • minimizing tech debt,
  • making sure dependencies are bug free,
  • security of your code, and the dependencies you use,
  • and also just being able to have access to developer and UX enhancements that are introduced.


So this article is for the developers who have an existing SvelteKit app made with a version below v2.0. We’ll first look at the important breaking API changes, and then walk through the process of upgrading. The Svelte team have released a tool which helps us through this process, however it’s important you get this right!

Skip to FAQ’s

Check if you’re already using SvelteKit v2

First of all, let’s make sure you’re not already running version 2.

If you made your project before the 14th December 2023, it’s likely you are running an earlier 1.x.x version, and should follow this migration guide.

You can check your exact version going to your package.json file. Find the @sveltejs/kit package in your devDependencies, and see if the version is under 2 (ignoring any symbol, like the carrot ^):

Check your current SvelteKit version

If your version is 2.0.0 or above then you don’t need to go through this guide.

But for the rest of us, I’ll walk you through each step to get to the wonderful land of version 2.



There are a few things you must make sure of before we begin the process of migrating to v2, and these are:

  1. You must be using Svelte v4.
  2. It’s recommended you are running the latest version of SvelteKit 1.x
  3. You must be Node version 18.13 or above

Make sure you are using Svelte v4

As SvelteKit uses the Svelte language to build components, it is required you use at least Svelte v4.0.

You can check your current version of Svelte in your project by going to your package.json file and looking for svelte in your devDependencies.

Check your current Svelte version

If the version you currently run is less than 4.0, follow the Svelte 4 Migration Guide to get yourself up to speed.

Update to the latest version of SvelteKit v1.x

Before doing the major jump to v2, you’ll likely have a few deprecations inside your own app at the v1 level that should be ironed out.

You can do this in your terminal by running running the following command:

npm install @sveltejs/kit@1

Which will install the latest 1.x.x version. Or if you’re running yarn or pnpm, run:

  • Yarn: yarn add @sveltejs/kit@1,
  • pnpm: `pnpm add @sveltejs/kit@1

Then try to build your app with:

npm run build

And see if the compiler yells at you. If so, fix the specific issues and then we can move on.

Check you’re using Node Version 18.13 or higher

You can check the version of Node in your terminal by running the following command:

node -v

And it will spit out the node version you’re running on your machine.

Check your current Node version

If you need to upgrade your node version, there’s a few ways, but the easier way to do it is:

Install latest Node version in Windows

The easiest way to update to the latest Node version with Windows is to reinstall from the .msi file on the Node website.

How to update Node in Windows

If you click the button labelled “Recommended For Most Users”, it’ll download the .msi file, which you can double-click when finished, and follow the default install.

Once this is done, you should be compliant with the SvelteKit v2 requirements!

Install latest Node version with Mac/Linux

With Mac or Linux, it’s as simple as running this following command

npm install n -g

Once it’s finished downloading and installing, you should have the latest Node version which is required by SvelteKit v2.

The dependency list

If we’ve done everything correctly so far, we should be ready to get started with the main part - the actual migration to v2!

But for reference, here is a list of the updated dependency requirements when moving to version 2.

The process we are about to use will update this all for you, however it’s there if you want to deeper understand what’s happening.

Ok, let’s migrate!


The Migration Process

The Svelte team have built the svelte-migrate to help us update version seemlessly.

The svelte-migrate tool

This tool will take care of most of the changes under the hood, including updating all the necessary dependencies, and also making changes to the compiler options in your tsconfig.json file.

We can use this tool from our terminal by running the following command:

npx svelte-migrate@latest sveltekit-2

This will migrate some of the changes automatically, without having to worry about what’s happening under the hood.


This includes updating your package.json, and some other configuration changes, including:

  • updating the tsconfig.json, which now uses "moduleResolution": "bundler", and
  • the verbatimModuleSyntax, which replaces the importsNotUsedAsValues and preserveValueImports flags in your tsconfig.json.

If you’re not using the svelte-migrate tool, and have either of the importsNotUsedAsValues and preserveValueImports flags in your tsconfig.json compilerOptions, you should remove them. You should also make sure in the compilerOptions in your tsconfig.json file has the property "moduleResolution": "bundler", as shown below:

Check your moduleResolution

That should be the automatic changes done for you, so now we’ll need to work on the manual stuff!

Things you’ll need to change manually

The parts of our code base which we’ll need to change are the following:

  1. Every instance of throw error or throw redirect should remove the throw, i.e. they must change to error, and redirect, respectively.
  2. When setting cookies, you must include a path property in the cookies.set options.
  3. An await must be added to top-level promises if your load functions should be blocking.
  4. Make sure any instances of the goto() with an external URL are changed to window.location = url.
  5. Make sure paths are either always relative, or always absolute, depending on the value of your paths.relative property in your svelte.config.js.
  6. Remove any instances of server tracking URL’s in your fetch functions, and remove the dangerZone.trackServerFetches, as it no longer exists.
  7. preloadCode can also only take 1 argument now - the pathname, which must be prefixed with the base path, similarly to preloadData.
  8. Change all instances of resolvePath to resolveRoute and make sure you understand that it now it includes the base in the resolved route - so if you have base + resolvePath(), it should now become resolveRoute().
  9. Change dynamic environment variables ($env/dynamic/public, and $env/dynamic/private) to static environment variables ($env/static/public, and $env/static/private) where possible. Dynamic variables aren’t used for prerendering anymore.
  10. In all instances of calling the use:enhance callback for forms, if the form or data properties are used, they must be changed to formElement and formData respectively.
  11. Make sure the attribute enctype="multipart/form-data" is added to all file inputs (i.e. <input type="file">) in your forms. Without this attribute, any non-JS submissions will omit the file when submitting.
  12. If you use either paths or baseUrl in your tsconfig.json file, make sure you convert them to the alias option.
  13. Update your vitePreprocess imports from import { vitePreprocess } from '@sveltejs/kit/vite' to import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'. This should be imported in your svelte.config.js file.

So there you have it. Some of the changes would have already happened with svelte-migrate, however most of these will be quick fixes, and the whole process should take no more than 10 minutes for most apps.


For the rest of this article, we’ll walk through these steps in more detail so that you should have no problems.

1. Remove the throw for error and redirect

This is straightforward. You simply must remove the throw keyword whenever using it in conjunction with error or redirect throughout your entire app.

If you’re using VS code, you can find all instances of this easily in your sidebar.

Click the Search Icon (magnifying glass) to open up your “search through codebase” panel:

Click the search icon in VS Code

Once you do this, you should see the search panel pop out. Then you should type throw error into the search box:

How to search for a string in your codebase

And below it, you’ll see a list of every instance of throw error in your app, which you can click on and it’ll take you precisely to the line, where you can remove throw.

If you’re a little gung-ho, you can type error in the “Replace” row in the search panel, and click the “replace all” button next to the input to replace all instances of throw error to error in your codebase:

Use the replace-all feature

While this is convenient, it’s slightly risky as you might be editing something you don’t mean to. It should be safe in 99% of cases, however sometimes it’s good to eyeball every change.

And once you’ve done that, you just need to repeat the process with throw redirect. Where you need to just use the keyword redirect.

NOTE: We’ll use this search feature a lot throughout the rest of the guide, so just remember this when “search for all instances” gets mentioned below.

2. Include a path when setting cookies

We need to first search for all instances of cookies.set in our codebase.

If any any instance is missing options with the path property, and add it. The value you use for path is the scoping to which the cookie gets applied. So if you want the cookie to apply to your entire app, use /:

cookies.set(name, value, { path: '/' });

If you want it to only apply to the current path, then use '':

cookies.set(name, value, { path: '' });

Or for the current directory, .:

cookies.set(name, value, { path: '.' });

If you want to get more advanced with your cookie scoping, see the Mozilla web docs for path details.

3. Add an await to top-level promises in your load functions

If you want blocking behaviour in your load functions, you now need to await them.

In SvelteKit v1, you could return the promise, and it would await it for you.

For example, you might have a load function that looks like this:

export function load({ fetch }) {
	const response = fetch(endpoint).then((r) => r.json());
	return { response };

Where the response is was automatically awaited before being sent to the client.

However, now that load function streaming is available in SvelteKit, it is no longer awaited, so you’ll need to explicitly await the fetch call, otherwise you’ll pass the streamed object to the client, rather than the result


You can do that by making the load function async and adding await before the fetch keyword:

export async function load({ fetch }) {
	const response = await fetch(endpoint).then((r) => r.json());
	return { response };

Now the response will be blocking, and you can send the fetch response to the client, rather than a streamed promise.

Awaiting several promises in your load functions

Now that you must await promises in your load functions, this means that waterfalls can begin to occur when you make the changes mentioned above for instances where there are multiple promises.

In order to prevent the waterfall that would occur with this behavior, you can make use of Promise.all() if you need to await many fetches.

For example, if you were to await a series of promises like this:

export async function load({ fetch }) {
	const responseA = await fetch(endpointA).then((r) => r.json());
	const responseB = await fetch(endpointB).then((r) => r.json());
	const responseC = await fetch(endpointC).then((r) => r.json());
	return { responseA, responseB, responseC };

You’re going to be waiting a long time, as endpointB only begins fetching once the response of endpointA is returned, and the same goes for responseC.


But if you utilize a Promise.all() like this:

export async function load({ fetch }) {
	const [responseA, responseB, responseC] = await Promise.all([
		fetch(endpointA).then((r) => r.json()),
		fetch(endpointB).then((r) => r.json()),
		fetch(endpointC).then((r) => r.json())

	return { responseA, reponseB, responseC };

You won’t have a waterfall between the fetch calls, and your wait time will only be as long as the longest fetch call.

4. Change goto(externalUrl) to window.location

The goto() helper is a way to programmatically navigate to pages inside your app.


In SvelteKit v1 you could use it to navigate away from your app, however with the introduction of Shallow Routing in v2, the interface has changed.

So, use the search feature in your IDE to find all instances of goto, and make sure each of their parameters is to a URL inside your app.

If it is to a URL outside of your app, change it from this:

await goto(externalUrl);

to this:

await window.location = externalUrl

5. Make sure paths are either always relative, or always absolute

First, we need to see what’s happening in the config of your svelte.config.js file.

Look for const config = {...}. It should look something like this:

const config = {
	extensions: ['.svelte'],
	preprocess: [vitePreprocess()],

	kit: {
		adapter: adapter(),
		paths: {
			relative: true

Perhaps in your config.kit options, you haven’t declared a paths option, which is fine. By default, paths.relative is set to true, and therefore you must use relative directories when importing from $app/paths throughout your app.


6. Remove instances of server tracking URL’s in fetch functions

You’ll need to look for dangerZone.trackServerFetches throughout your app and remove every instance.

So in your IDE’s search panel, search for dangerZone. If you have no instances where you used this in your app, then you are okay to proceed.


However, if you have this anywhere in your app, you’ll have to remove it as it’s not longer an option with v2.

The problem was that the fetched URL’s in server load functions could expose environment secrets if the URL contained them.

7. Only use 1 argument for preloadCode and prefix it with the base path

First, search for all instances of preloadCode in your app, and make sure it only has 1 argument. This argument should be the pathname, including your base path.

So if you were using relative paths for the preloadCode functions, you must change it to make sure the base path is now included.

8. Change resolvePath to resolveRoute and remove any instances of base with it

Once again, we’ll use the search feature to look for any instances of resolvePath in our app.

For each instance, we need to make sure of 1 things:

  1. the resolvePath keyword must become resolveRoute, and
  2. If you appended the base to the resolvePath value, you can remove it as the resolveRoute value will include the base from $app/paths


So if you have a line of code like this:

import { resolvePath } from '@sveltejs/kit';

const path = resolvePath('/blog/[slug]', { slug });

Or a block of code that looks like this:

import { resolvePath } from '@sveltejs/kit';
import { base } from '$app/paths';

const path = base + resolvePath('/blog/[slug]', { slug });

You can change both of them to this:

import { resolveRoute } from '$app/paths';

const path = resolveRoute('/blog/[slug]', { slug });

Because resolveRoute already includes the base that your were importing from $app/paths.

Also, note that you’ll need to update the import from @sveltejs/kit to $app/paths.

The svelte-migrate tool will do this for you, however in the second code block mentioned with base prepended to compose path, you’ll need to manually remove the ”base +” yourself.

9. Change Dynamic Environment Variables to Static where Possible

With SvelteKit v2, dynamic environment variables won’t be used for prerendering anymore. So if you require environment variables in your app, it’s a good idea to change every instance to import them statically at the build stage, where possible.


If you must import them dynamically, just know that when a prerendered page attempts to access a dynamic environment variable, it will request an up to date value from the server, rather than reading it from the server rendered HTML. This behavior can be configured with in your svelte.config.js file, inside config.kit.env.publicModule.

If you’re not sure whether you need the environment variable to be dynamic or static, you can get more information in the SvelteKit official docs.

How to update dynamic environment variables to static as a precaution

So, in your IDE search panel, search for all instances of $env/dynamic/public.

You’ll have to look at each instance manually and assess if the value that’s being imported would be the same at build time as to the time it gets accessed and used when your app is being used.

For example, let’s say your app accesses a Google API, and they give you a key to use in your app. As this key shouldn’t ever really change, you should be able to use it statically.

So, in this case, you would change:

import { ENV_VARIABLE } from $env/dynamic/public


import { ENV_VARIABLE } from $env/static/public

And the same goes for private environment variables ($env/dynamic/private). Just change dynamic to static, regardless of whether it’s public or private.

10. Change form to formElement and data to formData for the use:enhance callback

If you’ve used the SvelteKit use:enhance feature for form submissions, you often pass an object to the callback which contains a handful of properties. With SvelteKit v1, you could utilize form and data within your callback, however with v2, you’ll have to change it to formElement and formData, respectively.


You can find all instances of this in your project by searching use:enhance in your IDE’s Search Panel.

You won’t be able to use the ”Search and Replace all” method with step as there should be multiple instances of form and data that you’ll need to retain.

So go through each instance of use:enhance, and you should see something like this:

<form method="POST" use:enhance="{({" form, data, action, cancel, submitter })="">
	{         // Do something with callback object properties         return async ({ result, update
	}) => {             // Do something with result, or update         };     }} >    
	<!-- Form contents -->

In all instances you see this line:

use:enhance={({ form, data, action, cancel, submitter })

Make the changes:

  • form => formElement, and
  • data => formData

So that it should now look like this:

<form method="POST" use:enhance="{({" formElement, formData, action, cancel, submitter })="">
	{         // Do something with callback object properties         return async ({ result, update
	}) => {             // Do something with result, or update         };     }} >    
	<!-- Form contents -->

11. Add the enctype="multipart/form-data" attribute for all file inputs

If you wish to have your file submitted for your forms in non-Javascript environments, then you’ll need to add enctype="multipart/form-data" to each instance of your <input type="file" />.

So this:

<input type="file" />

will become this:

<input type="file" enctype="multipart/form-data" />

And now when a user without Javascript submits the form, the file won’t be omitted.


In order to find all instances of this in your project, I’d normally suggest you use the “search” feature in you IDE panel to search for <input or type="file", but you could be missing instances with this alone. So you’ll have to dig deep here and use the following search queries to find all instances of <input type="file" />:

  • <input,
  • type="file" (with double quotations ""),
  • type='file' (with single quotations ”),
  • type={"file"} (double quotes surrounded by braces),
  • type={'file'} (single quotes surrounded by braces),

And you should also check if you’re using a library or custom component which abstracts the file input away to its own component, for example <FileInput />.

For most projects, using the list of queries above is exhaustive enough, however it’s up to you to find every instances of this in your project as it could be implemented many different ways.

12. Move to the alias option from paths and baseUrl in your tsconfig.json file

In your tsconfig.json file, you might have the paths or baseUrl properties in your compilerOptions:

Check paths and baseUrl in your tsconfig

If you don’t have these properties in your tsconfig.json file then you can move onto the next migration step.

However, if you do have these properties there, then we want to:

  1. Remove both the paths and baseUrl properties in this tsconfig.json file, and
  2. Add the alias property to our svelte.config.json inside the config.kit object. How you configure your alias property should be to taste, however it’s worth understanding this alias property - which you can learn about in the SvelteKit docs.


So once you’ve configured your alias property in the svelte.config.json file, it should look something like this:

const config = {
	extensions: ['.svelte'],
	preprocess: [vitePreprocess()],

	kit: {
		adapter: adapter(),
		alias: {
			'@': 'src/lib/contacts.ts',
			components: 'src/lib/components'

Yours should be configured differently, however in this example, you should be able to import contacts with:

import contacts from '@';


import { Banner, Accordion } from 'components';

Moving away from the paths and baseUrl in the tsconfig.json towards an alias in the svelte.config.js file is more explicit, and SvelteKit v2 will warn you if you don’t do this.

Remember, you only need to do this step if you have the paths and baseUrl properties in your tsconfig.json file.

Most of the times, it’s fine leaving SvelteKit with the default settings and having none of the paths, baseUrl or even alias properties.

13. Update your viteProcess imports

Using your search panel in your IDE, search for vitePreprocess, and go through each instance where it says:

import { vitePreprocess } from '@sveltejs/kit/vite';

and change it to:

import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

For most projects, this should only be in your svelte.config.js file, however it can’t hurt to search for it if it exists more than one.


You made it! 🎉

You’ve now successfully migrated to the new shiny SvelteKit v2, and have access to all of its improvements and upgraded security.


Now that you’re here, check out some of the new features in SvelteKit v2 that are available to you.

I hope this detailed guide was helpful!


Thanks for reading ❤️

Here are some other articles I think you might like!