Repository: sveltejs/kit (js)

Issue:
`beforeNavigate` runs twice if redirecting in the navigation targets `load`
### Describe the bug

Don't know if this is a bug or a feature... But if I have a before navigate in a route. And then I redirect in the load function on the route we are navigating to. The beforeNavigate runs twice in the original route.

### Reproduction

It's reproduced here:
https://stackblitz.com/edit/sveltejs-kit-template-default-5ed6za?file=src/app.html

I've made this example of the bug occuring.
If we are at the `/` route (Home) and then go to `/about` (About) "BEFORE" is only logged once.
But if we go back to `/` and then go to `/test` (Test) we are correctly redirected to `/about` but "BEFORE" is logged twice.
The console looks like this for the latter case:
```
BEFORE
LOAD IN TEST
BEFORE
```

### Logs

_No response_

### System Info

```Shell
System:
    OS: Linux 5.0 undefined
    CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 16.14.2 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 7.17.0 - /usr/local/bin/npm
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.87 
    @sveltejs/kit: next => 1.0.0-next.539 
    svelte: ^3.46.0 => 3.52.0 
    vite: ^3.1.0 => 3.2.3
```


### Severity

serious, but I can work around it

### Additional Information

This is quite a serious bug for how we use `beforeNavigate` in our application. But can be worked around

Pull request:
[fix] don't run beforeNavigate during redirects
Fixes #7584

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [x] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/hungry-tips-obey.md b/.changeset/hungry-tips-obey.md
new file mode 100644
index 000000000..f59b225de
--- /dev/null
+++ b/.changeset/hungry-tips-obey.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] don't run beforeNavigate during redirects
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 9bc0b1ec6..361f52738 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -589,6 +589,7 @@
     "large-kangaroos-remain",
     "large-lies-reply",
     "large-seahorses-check",
+    "large-starfishes-relate",
     "late-bats-rule",
     "late-bobcats-brake",
     "late-cats-care",
@@ -972,6 +973,7 @@
     "rich-walls-doubt",
     "rotten-cats-pump",
     "rotten-cherries-jump",
+    "rotten-files-work",
     "rotten-gorillas-sort",
     "rotten-mice-compete",
     "rotten-rocks-mate",
diff --git a/.changeset/rotten-files-work.md b/.changeset/rotten-files-work.md
new file mode 100644
index 000000000..376ac7597
--- /dev/null
+++ b/.changeset/rotten-files-work.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] handle expected errors thrown in handle hook correctly
diff --git a/documentation/docs/20-core-concepts/10-routing.md b/documentation/docs/20-core-concepts/10-routing.md
index 95dcc880c..2d8b08e62 100644
--- a/documentation/docs/20-core-concepts/10-routing.md
+++ b/documentation/docs/20-core-concepts/10-routing.md
@@ -129,6 +129,10 @@ If an error occurs during `load`, SvelteKit will render a default error page. Yo
 
 SvelteKit will 'walk up the tree' looking for the closest error boundary — if the file above didn't exist it would try `src/routes/blog/+error.svelte` and `src/routes/+error.svelte` before rendering the default error page. If _that_ fails (or if the error was thrown from the `load` function of the root `+layout`, which sits 'above' the root `+error`), SvelteKit will bail out and render a static fallback error page, which you can customise by creating a `src/error.html` file.
 
+> `+error.svelte` is _not_ used when an error occurs inside [`handle`](/docs/hooks#server-hooks-handle) or a [+server.js](#server) request handler.
+
+You can read more about error handling [here](/docs/errors).
+
 ### +layout
 
 So far, we've treated pages as entirely standalone components — upon navigation, the existing `+page.svelte` component will be destroyed, and a new one will take its place.
@@ -267,7 +271,9 @@ export function GET({ url }) {
 
 The first argument to `Response` can be a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream), making it possible to stream large amounts of data or create server-sent events (unless deploying to platforms that buffer responses, like AWS Lambda).
 
-You can use the `error`, `redirect` and `json` methods from `@sveltejs/kit` for convenience (but you don't have to). Note that `throw error(..)` only returns a plain text error response.
+You can use the `error`, `redirect` and `json` methods from `@sveltejs/kit` for convenience (but you don't have to).
+
+If an error is thrown (either `throw error(...)` or an unexpected error), the response will be a JSON representation of the error or a fallback error page — which can be customised via `src/error.html` — depending on the `Accept` header. The [`+error.svelte`](#error) component will _not_ be rendered in this case. You can read more about error handling [here](/docs/errors).
 
 #### Receiving data
 
diff --git a/documentation/docs/20-core-concepts/40-page-options.md b/documentation/docs/20-core-concepts/40-page-options.md
index 9ad9153d1..f777c6d36 100644
--- a/documentation/docs/20-core-concepts/40-page-options.md
+++ b/documentation/docs/20-core-concepts/40-page-options.md
@@ -62,6 +62,8 @@ Note that you can still prerender pages that load data based on the page's param
 
 Accessing [`url.searchParams`](/docs/load#using-url-data-url) during prerendering is forbidden. If you need to use it, ensure you are only doing so in the browser (for example in `onMount`).
 
+Pages with [actions](/docs/form-actions) cannot be prerendered, because a server must be able to handle the action `POST` requests.
+
 #### Route conflicts
 
 Because prerendering writes to the filesystem, it isn't possible to have two endpoints that would cause a directory and a file to have the same name. For example, `src/routes/foo/+server.js` and `src/routes/foo/bar/+server.js` would try to create `foo` and `foo/bar`, which is impossible.
diff --git a/documentation/docs/30-advanced/20-hooks.md b/documentation/docs/30-advanced/20-hooks.md
index 3cc20fbf9..727716f83 100644
--- a/documentation/docs/30-advanced/20-hooks.md
+++ b/documentation/docs/30-advanced/20-hooks.md
@@ -86,6 +86,8 @@ export async function handle({ event, resolve }) {
 }
 ```
 
+Note that `resolve(...)` will never throw an error, it will always return a `Promise<Response>` with the appropriate status code. If an error is thrown elsewhere during `handle`, it is treated as fatal, and SvelteKit will respond with a JSON representation of the error or a fallback error page — which can be customised via `src/error.html` — depending on the `Accept` header. You can read more about error handling [here](/docs/errors).
+
 #### handleFetch
 
 This function allows you to modify (or replace) a `fetch` request that happens inside a `load` function that runs on the server (or during pre-rendering).
@@ -93,6 +95,7 @@ This function allows you to modify (or replace) a `fetch` request that happens i
 Or your `load` function might make a request to a public URL like `https://api.yourapp.com` when the user performs a client-side navigation to the respective page, but during SSR it might make sense to hit the API directly (bypassing whatever proxies and load balancers sit between it and the public internet).
 
 ```js
+/// file: src/hooks.server.js
 /** @type {import('@sveltejs/kit').HandleFetch} */
 export async function handleFetch({ request, fetch }) {
 	if (request.url.startsWith('https://api.yourapp.com/')) {
diff --git a/documentation/docs/30-advanced/25-errors.md b/documentation/docs/30-advanced/25-errors.md
new file mode 100644
index 000000000..3526ea9e7
--- /dev/null
+++ b/documentation/docs/30-advanced/25-errors.md
@@ -0,0 +1,143 @@
+---
+title: Errors
+---
+
+Errors are an inevitable fact of software development. SvelteKit handles errors differently depending on where they occur, what kind of errors they are, and the nature of the incoming request.
+
+### Error objects
+
+SvelteKit distinguishes between expected and unexpected errors, both of which are represented as simple `{ message: string }` objects by default.
+
+You can add additional properties, like a `code` or a tracking `id`, as shown below.
+
+### Expected errors
+
+An _expected_ error is one created with the [`error`](/docs/modules#sveltejs-kit-error) helper imported from `@sveltejs/kit`:
+
+```js
+/// file: src/routes/blog/[slug]/+page.server.js
+// @filename: ambient.d.ts
+declare module '$lib/server/database' {
+	export function getPost(slug: string): Promise<{ title: string, content: string } | undefined>
+}
+
+// @filename: index.js
+// ---cut---
+import { error } from '@sveltejs/kit';
+import * as db from '$lib/server/database';
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load({ params }) {
+	const post = await db.getPost(params.slug);
+
+	if (!post) {
+		throw error(404, {
+			message: 'Not found'
+		});
+	}
+
+	return { post };
+}
+```
+
+This tells SvelteKit to set the response status code to 404 and render an [`+error.svelte`](/docs/routing#error) component, where `$page.error` is the object provided as the second argument to `error(...)`.
+
+```svelte
+/// file: src/routes/+error.svelte
+<script>
+	import { page } from '$app/stores';
+</script>
+
+<h1>{$page.error.message}</h1>
+```
+
+You can add extra properties to the error object if needed...
+
+```diff
+throw error(404, {
+	message: 'Not found',
++	code: 'NOT_FOUND'
+});
+```
+
+...otherwise, for convenience, you can pass a string as the second argument:
+
+```diff
+-throw error(404, { message: 'Not found' });
++throw error(404, 'Not found');
+```
+
+### Unexpected errors
+
+An _unexpected_ error is any other exception that occurs while handling a request. Since these can contain sensitive information, unexpected error messages and stack traces are not exposed to users.
+
+By default, unexpected errors are printed to the console (or, in production, your server logs), while the error that is exposed to the user has a generic shape:
+
+```json
+{ "message": "Internal Error" }
+```
+
+Unexpected errors will go through the [`handleError`](/docs/hooks#shared-hooks-handleerror) hook, where you can add your own error handling — for example, sending errors to a reporting service, or returning a custom error object. 
+
+```js
+/// file: src/hooks.server.js
+// @errors: 2322 2571
+// @filename: ambient.d.ts
+const Sentry: any;
+
+// @filename: index.js
+// ---cut---
+/** @type {import('@sveltejs/kit').HandleServerError} */
+export function handleError({ error, event }) {
+	// example integration with https://sentry.io/
+	Sentry.captureException(error, { event });
+
+	return {
+		message: 'Whoops!',
+		code: error.code ?? 'UNKNOWN'
+	};
+}
+```
+
+### Responses
+
+If an error occurs inside `handle` or inside a [`+server.js`](/docs/routing#server) request handler, SvelteKit will respond with either a fallback error page or a JSON representation of the error object, depending on the request's `Accept` headers.
+
+You can customise the fallback error page by adding a `src/error.html` file:
+
+```html
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<title>%sveltekit.error.message%</title>
+	</head>
+	<body>
+		<h1>My custom error page</h1>
+		<p>Status: %sveltekit.status%</p>
+		<p>Message: %sveltekit.error.message%</p>
+	</body>
+</html>
+```
+
+SvelteKit will replace `%sveltekit.status%` and `%sveltekit.error.message%` with their corresponding values.
+
+If the error instead occurs inside a `load` function while rendering a page, SvelteKit will render the [`+error.svelte`](/docs/routing#error) component nearest to where the error occurred.
+
+The exception is when the error occurs inside the root `+layout.js` or `+layout.server.js`, since the root layout would ordinarily _contain_ the `+error.svelte` component. In this case, SvelteKit uses the fallback error page.
+
+### Type safety
+
+If you're using TypeScript and need to customize the shape of errors, you can do so by declaring an `App.Error` interface in your app (by convention, in `src/app.d.ts`, though it can live anywhere that TypeScript can 'see'):
+
+```ts
+/// file: src/app.d.ts
+namespace App {
+	interface Error {
+		code: string;
+		id: string;
+	}
+}
+```
+
+This interface always includes a `message: string` property.
\ No newline at end of file
diff --git a/documentation/docs/60-appendix/30-glossary.md b/documentation/docs/60-appendix/30-glossary.md
index 6a5b3d9d1..4b94675a6 100644
--- a/documentation/docs/60-appendix/30-glossary.md
+++ b/documentation/docs/60-appendix/30-glossary.md
@@ -16,7 +16,7 @@ Client-side rendering (CSR) is the generation of the page contents in the web br
 
 Prerendering means computing the contents of a page at build time and saving the HTML for display. This approach has the same benefits as traditional server-rendered pages, but avoids recomputing the page for each visitor and so scales nearly for free as the number of visitors increases. The tradeoff is that the build process is more expensive and prerendered content can only be updated by building and deploying a new version of the application.
 
-Not all pages can be prerendered. The basic rule is this: for content to be prerenderable, any two users hitting it directly must get the same content from the server. Note that you can still prerender content that is loaded based on the page's parameters as long as all users will be seeing the same prerendered content.
+Not all pages can be prerendered. The basic rule is this: for content to be prerenderable, any two users hitting it directly must get the same content from the server, and the page must not contain [actions](/docs/form-actions). Note that you can still prerender content that is loaded based on the page's parameters as long as all users will be seeing the same prerendered content.
 
 Pre-rendered pages are not limited to static content. You can build personalized pages if user-specific data is fetched and rendered client-side. This is subject to the caveat that you will experience the downsides of not doing SSR for that content as discussed above.
 
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 9005b806f..f2f0050fc 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,15 @@
 # @sveltejs/kit
 
+## 1.0.0-next.541
+
+### Minor Changes
+
+- [fix] don't clean build and output directories when using `--watch` ([#7516](https://github.com/sveltejs/kit/pull/7516))
+
+### Patch Changes
+
+- [fix] handle expected errors thrown in handle hook correctly ([#7566](https://github.com/sveltejs/kit/pull/7566))
+
 ## 1.0.0-next.540
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index c9fae0d78..611fef465 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.540",
+	"version": "1.0.0-next.541",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index e871bbf39..ed26e5440 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -1066,7 +1066,10 @@ export function create_client({ target, base, trailing_slash }) {
 			}
 		};
 
-		callbacks.before_navigate.forEach((fn) => fn(cancellable));
+		if (!navigating) {
+			// Don't run the event during redirects
+			callbacks.before_navigate.forEach((fn) => fn(cancellable));
+		}
 
 		return should_block ? null : navigation;
 	}
diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js
index 07f0a9efd..a729ab6a4 100644
--- a/packages/kit/src/runtime/server/endpoint.js
+++ b/packages/kit/src/runtime/server/endpoint.js
@@ -64,8 +64,6 @@ export async function render_endpoint(event, mod, state) {
 				status: error.status,
 				headers: { location: error.location }
 			});
-		} else if (error instanceof ValidationError) {
-			return json(error.data, { status: error.status });
 		}
 
 		throw error;
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index 4e791508a..6385ae8b7 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -16,7 +16,6 @@ import {
 import { exec } from '../../utils/routing.js';
 import { INVALIDATED_HEADER, render_data } from './data/index.js';
 import { add_cookies_to_headers, get_cookies } from './cookie.js';
-import { HttpError } from '../control.js';
 import { create_fetch } from './fetch.js';
 
 /* global __SVELTEKIT_ADAPTER_NAME__ */
@@ -275,9 +274,8 @@ export async function respond(request, options, state) {
 			// we can't load the endpoint from our own manifest,
 			// so we need to make an actual HTTP request
 			return await fetch(request);
-		} catch (e) {
-			// HttpError can come from endpoint - TODO should it be handled there instead?
-			const error = e instanceof HttpError ? e : coalesce_to_error(e);
+		} catch (error) {
+			// HttpError from endpoint can end up here - TODO should it be handled there instead?
 			return handle_fatal_error(event, options, error);
 		} finally {
 			event.cookies.set = () => {
@@ -362,8 +360,7 @@ export async function respond(request, options, state) {
 		}
 
 		return response;
-	} catch (/** @type {unknown} */ e) {
-		const error = coalesce_to_error(e);
+	} catch (error) {
 		return handle_fatal_error(event, options, error);
 	}
 }
diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js
index d78c05cd7..488300d14 100644
--- a/packages/kit/src/runtime/server/utils.js
+++ b/packages/kit/src/runtime/server/utils.js
@@ -1,4 +1,5 @@
 import * as devalue from 'devalue';
+import { coalesce_to_error } from '../../utils/error.js';
 import { negotiate } from '../../utils/http.js';
 import { has_data_suffix } from '../../utils/url.js';
 import { HttpError } from '../control.js';
@@ -110,9 +111,10 @@ export function static_error_page(options, status, message) {
 /**
  * @param {import('types').RequestEvent} event
  * @param {import('types').SSROptions} options
- * @param {Error | HttpError} error
+ * @param {unknown} error
  */
 export function handle_fatal_error(event, options, error) {
+	error = error instanceof HttpError ? error : coalesce_to_error(error);
 	const status = error instanceof HttpError ? error.status : 500;
 	const body = handle_error_and_jsonify(event, options, error);
 


Test patch:
diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js
index 564ff59d5..d7541ef1d 100644
--- a/packages/kit/test/apps/basics/src/hooks.server.js
+++ b/packages/kit/test/apps/basics/src/hooks.server.js
@@ -1,6 +1,7 @@
 import fs from 'fs';
 import { sequence } from '@sveltejs/kit/hooks';
 import { HttpError } from '../../../../src/runtime/control';
+import { error } from '@sveltejs/kit';
 
 /**
  * Transform an error into a POJO, by copying its `name`, `message`
@@ -62,6 +63,8 @@ export const handle = sequence(
 	async ({ event, resolve }) => {
 		if (event.url.pathname === '/errors/error-in-handle') {
 			throw new Error('Error in handle');
+		} else if (event.url.pathname === '/errors/expected-error-in-handle') {
+			throw error(500, 'Expected error in handle');
 		}
 
 		const response = await resolve(event, {
diff --git a/packages/kit/test/apps/basics/src/routes/before-navigate/prevent-navigation/+page.svelte b/packages/kit/test/apps/basics/src/routes/before-navigate/prevent-navigation/+page.svelte
index 829056d1f..402bc60cf 100644
--- a/packages/kit/test/apps/basics/src/routes/before-navigate/prevent-navigation/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/before-navigate/prevent-navigation/+page.svelte
@@ -1,18 +1,21 @@
 <script>
 	import { beforeNavigate } from '$app/navigation';
 
-	let triggered = false;
+	let times_triggered = 0;
 	let unload = false;
 	let navigation_type;
-	beforeNavigate(({ cancel, type, willUnload }) => {
-		triggered = true;
+	beforeNavigate(({ cancel, type, willUnload, to }) => {
+		times_triggered++;
 		unload = willUnload;
 		navigation_type = type;
-		cancel();
+		if (!to?.route.id?.includes('redirect')) {
+			cancel();
+		}
 	});
 </script>
 
 <h1>prevent navigation</h1>
 <a href="/before-navigate/a">a</a>
+<a href="/before-navigate/redirect">redirect</a>
 <a href="https://google.de">external</a>
-<pre>{triggered} {unload} {navigation_type}</pre>
+<pre>{times_triggered} {unload} {navigation_type}</pre>
diff --git a/packages/kit/test/apps/basics/src/routes/before-navigate/redirect/+page.js b/packages/kit/test/apps/basics/src/routes/before-navigate/redirect/+page.js
new file mode 100644
index 000000000..a5b9dcc34
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/before-navigate/redirect/+page.js
@@ -0,0 +1,5 @@
+import { redirect } from '@sveltejs/kit';
+
+export function load() {
+	throw redirect(307, '/before-navigate/prevent-navigation');
+}
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 8a04dd861..1d8e48f2b 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -27,7 +27,7 @@ test.describe('beforeNavigate', () => {
 		await page.waitForLoadState('networkidle');
 
 		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
-		expect(await page.innerHTML('pre')).toBe('true false link');
+		expect(await page.innerHTML('pre')).toBe('1 false link');
 	});
 
 	test('prevents navigation to external', async ({ page, baseURL }) => {
@@ -38,7 +38,7 @@ test.describe('beforeNavigate', () => {
 
 		page.click('a[href="https://google.de"]'); // do NOT await this, promise only resolves after successful navigation, which never happens
 		await page.waitForTimeout(500);
-		await expect(page.locator('pre')).toHaveText('true true link');
+		await expect(page.locator('pre')).toHaveText('1 true link');
 		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
 	});
 
@@ -46,14 +46,14 @@ test.describe('beforeNavigate', () => {
 		await page.goto('/before-navigate/prevent-navigation');
 		await app.goto('/before-navigate/a');
 		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
-		expect(await page.innerHTML('pre')).toBe('true false goto');
+		expect(await page.innerHTML('pre')).toBe('1 false goto');
 	});
 
 	test('prevents external navigation triggered by goto', async ({ page, app, baseURL }) => {
 		await page.goto('/before-navigate/prevent-navigation');
 		await app.goto('https://google.de');
 		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
-		expect(await page.innerHTML('pre')).toBe('true true goto');
+		expect(await page.innerHTML('pre')).toBe('1 true goto');
 	});
 
 	test('prevents navigation triggered by back button', async ({ page, app, baseURL }) => {
@@ -62,7 +62,7 @@ test.describe('beforeNavigate', () => {
 		await page.click('h1'); // The browsers block attempts to prevent navigation on a frame that's never had a user gesture.
 
 		await page.goBack();
-		expect(await page.innerHTML('pre')).toBe('true false popstate');
+		expect(await page.innerHTML('pre')).toBe('1 false popstate');
 		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
 	});
 
@@ -78,7 +78,17 @@ test.describe('beforeNavigate', () => {
 
 		await page.close({ runBeforeUnload: true });
 		expect(await type).toBe('beforeunload');
-		expect(await page.innerHTML('pre')).toBe('true true leave');
+		expect(await page.innerHTML('pre')).toBe('1 true leave');
+	});
+
+	test('is not triggered on redirect', async ({ page, baseURL }) => {
+		await page.goto('/before-navigate/prevent-navigation');
+
+		await page.click('[href="/before-navigate/redirect"]');
+		await page.waitForLoadState('networkidle');
+
+		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
+		expect(await page.innerHTML('pre')).toBe('1 false link');
 	});
 });
 
diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js
index 2891ac916..44c8a52fe 100644
--- a/packages/kit/test/apps/basics/test/server.test.js
+++ b/packages/kit/test/apps/basics/test/server.test.js
@@ -258,6 +258,37 @@ test.describe('Errors', () => {
 			});
 		}
 	});
+
+	test('expected error thrown in handle results in a rendered error page or JSON response', async ({
+		request
+	}) => {
+		// HTML
+		{
+			const res = await request.get('/errors/expected-error-in-handle', {
+				headers: {
+					accept: 'text/html'
+				}
+			});
+
+			expect(res.status()).toBe(500);
+			expect(await res.text()).toContain(
+				'This is the static error page with the following message: Expected error in handle'
+			);
+		}
+
+		// JSON (default)
+		{
+			const res = await request.get('/errors/expected-error-in-handle');
+
+			const error = await res.json();
+
+			expect(error.stack).toBe(undefined);
+			expect(res.status()).toBe(500);
+			expect(error).toEqual({
+				message: 'Expected error in handle'
+			});
+		}
+	});
 });
 
 test.describe('Load', () => {