diff --git a/e2e/react-start/basic/src/routeTree.gen.ts b/e2e/react-start/basic/src/routeTree.gen.ts
index e29bb26d3c..1f798d17fa 100644
--- a/e2e/react-start/basic/src/routeTree.gen.ts
+++ b/e2e/react-start/basic/src/routeTree.gen.ts
@@ -34,6 +34,7 @@ import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/
 import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
 import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
 import { Route as NotFoundViaLoaderRouteImport } from './routes/not-found/via-loader'
+import { Route as NotFoundViaHeadRouteImport } from './routes/not-found/via-head'
 import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/via-beforeLoad'
 import { Route as ApiUsersRouteImport } from './routes/api.users'
 import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
@@ -169,6 +170,11 @@ const NotFoundViaLoaderRoute = NotFoundViaLoaderRouteImport.update({
   path: '/via-loader',
   getParentRoute: () => NotFoundRouteRoute,
 } as any)
+const NotFoundViaHeadRoute = NotFoundViaHeadRouteImport.update({
+  id: '/via-head',
+  path: '/via-head',
+  getParentRoute: () => NotFoundRouteRoute,
+} as any)
 const NotFoundViaBeforeLoadRoute = NotFoundViaBeforeLoadRouteImport.update({
   id: '/via-beforeLoad',
   path: '/via-beforeLoad',
@@ -272,6 +278,7 @@ export interface FileRoutesByFullPath {
   '/대한민국': typeof Char45824Char54620Char48124Char44397Route
   '/api/users': typeof ApiUsersRouteWithChildren
   '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+  '/not-found/via-head': typeof NotFoundViaHeadRoute
   '/not-found/via-loader': typeof NotFoundViaLoaderRoute
   '/posts/$postId': typeof PostsPostIdRoute
   '/redirect/$target': typeof RedirectTargetRouteWithChildren
@@ -307,6 +314,7 @@ export interface FileRoutesByTo {
   '/대한민국': typeof Char45824Char54620Char48124Char44397Route
   '/api/users': typeof ApiUsersRouteWithChildren
   '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+  '/not-found/via-head': typeof NotFoundViaHeadRoute
   '/not-found/via-loader': typeof NotFoundViaLoaderRoute
   '/posts/$postId': typeof PostsPostIdRoute
   '/search-params/default': typeof SearchParamsDefaultRoute
@@ -347,6 +355,7 @@ export interface FileRoutesById {
   '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
   '/api/users': typeof ApiUsersRouteWithChildren
   '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+  '/not-found/via-head': typeof NotFoundViaHeadRoute
   '/not-found/via-loader': typeof NotFoundViaLoaderRoute
   '/posts/$postId': typeof PostsPostIdRoute
   '/redirect/$target': typeof RedirectTargetRouteWithChildren
@@ -389,6 +398,7 @@ export interface FileRouteTypes {
     | '/대한민국'
     | '/api/users'
     | '/not-found/via-beforeLoad'
+    | '/not-found/via-head'
     | '/not-found/via-loader'
     | '/posts/$postId'
     | '/redirect/$target'
@@ -424,6 +434,7 @@ export interface FileRouteTypes {
     | '/대한민국'
     | '/api/users'
     | '/not-found/via-beforeLoad'
+    | '/not-found/via-head'
     | '/not-found/via-loader'
     | '/posts/$postId'
     | '/search-params/default'
@@ -463,6 +474,7 @@ export interface FileRouteTypes {
     | '/_layout/_layout-2'
     | '/api/users'
     | '/not-found/via-beforeLoad'
+    | '/not-found/via-head'
     | '/not-found/via-loader'
     | '/posts/$postId'
     | '/redirect/$target'
@@ -673,6 +685,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof NotFoundViaLoaderRouteImport
       parentRoute: typeof NotFoundRouteRoute
     }
+    '/not-found/via-head': {
+      id: '/not-found/via-head'
+      path: '/via-head'
+      fullPath: '/not-found/via-head'
+      preLoaderRoute: typeof NotFoundViaHeadRouteImport
+      parentRoute: typeof NotFoundRouteRoute
+    }
     '/not-found/via-beforeLoad': {
       id: '/not-found/via-beforeLoad'
       path: '/via-beforeLoad'
@@ -797,12 +816,14 @@ declare module '@tanstack/react-router' {
 
 interface NotFoundRouteRouteChildren {
   NotFoundViaBeforeLoadRoute: typeof NotFoundViaBeforeLoadRoute
+  NotFoundViaHeadRoute: typeof NotFoundViaHeadRoute
   NotFoundViaLoaderRoute: typeof NotFoundViaLoaderRoute
   NotFoundIndexRoute: typeof NotFoundIndexRoute
 }
 
 const NotFoundRouteRouteChildren: NotFoundRouteRouteChildren = {
   NotFoundViaBeforeLoadRoute: NotFoundViaBeforeLoadRoute,
+  NotFoundViaHeadRoute: NotFoundViaHeadRoute,
   NotFoundViaLoaderRoute: NotFoundViaLoaderRoute,
   NotFoundIndexRoute: NotFoundIndexRoute,
 }
diff --git a/e2e/react-start/basic/src/routes/not-found/index.tsx b/e2e/react-start/basic/src/routes/not-found/index.tsx
index e754f83c74..722813fca3 100644
--- a/e2e/react-start/basic/src/routes/not-found/index.tsx
+++ b/e2e/react-start/basic/src/routes/not-found/index.tsx
@@ -25,6 +25,16 @@ export const Route = createFileRoute('/not-found/')({
             via-loader
           </Link>
         </div>
+        <div className="mb-2">
+          <Link
+            from={Route.fullPath}
+            to="./via-head"
+            preload={preload}
+            data-testid="via-head"
+          >
+            via-head
+          </Link>
+        </div>
       </div>
     )
   },
diff --git a/e2e/react-start/basic/src/routes/not-found/via-head.tsx b/e2e/react-start/basic/src/routes/not-found/via-head.tsx
new file mode 100644
index 0000000000..7cd09f9fa3
--- /dev/null
+++ b/e2e/react-start/basic/src/routes/not-found/via-head.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, notFound } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/not-found/via-head')({
+  head: () => {
+    throw notFound()
+  },
+  component: RouteComponent,
+  notFoundComponent: () => {
+    return (
+      <div data-testid="via-head-notFound-component">
+        Not Found "/not-found/via-head"!
+      </div>
+    )
+  },
+})
+
+function RouteComponent() {
+  return (
+    <div data-testid="via-head-route-component" data-server={typeof window}>
+      Hello "/not-found/via-head"!
+    </div>
+  )
+}
diff --git a/e2e/react-start/basic/tests/not-found.spec.ts b/e2e/react-start/basic/tests/not-found.spec.ts
index bde0a1a02a..0b5acc3b78 100644
--- a/e2e/react-start/basic/tests/not-found.spec.ts
+++ b/e2e/react-start/basic/tests/not-found.spec.ts
@@ -9,6 +9,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport
 test.use({
   whitelistErrors: [
     /Failed to load resource: the server responded with a status of 404/,
+    'NotFound error during hydration for routeId',
   ],
 })
 test.describe('not-found', () => {
@@ -25,7 +26,7 @@ test.describe('not-found', () => {
   test.describe('throw notFound()', () => {
     const navigationTestMatrix = combinate({
       // TODO beforeLoad!
-      thrower: [/* 'beforeLoad',*/ 'loader'] as const,
+      thrower: [/* 'beforeLoad',*/ 'head', 'loader'] as const,
       preload: [false, true] as const,
     })
 
@@ -57,7 +58,7 @@ test.describe('not-found', () => {
     const directVisitTestMatrix = combinate({
       // TODO beforeLoad!
 
-      thrower: [/* 'beforeLoad',*/ 'loader'] as const,
+      thrower: [/* 'beforeLoad',*/ 'head', 'loader'] as const,
     })
 
     directVisitTestMatrix.forEach(({ thrower }) => {
diff --git a/e2e/react-start/basic/vite.config.ts b/e2e/react-start/basic/vite.config.ts
index f9249150d9..ecab96f2d8 100644
--- a/e2e/react-start/basic/vite.config.ts
+++ b/e2e/react-start/basic/vite.config.ts
@@ -20,6 +20,7 @@ const prerenderConfiguration = {
       '/redirect',
       '/i-do-not-exist',
       '/not-found/via-beforeLoad',
+      '/not-found/via-head',
       '/not-found/via-loader',
       '/users',
     ].some((p) => page.path.includes(p)),
diff --git a/e2e/solid-start/basic/src/routeTree.gen.ts b/e2e/solid-start/basic/src/routeTree.gen.ts
index 1fb482f3af..f1ecc1d149 100644
--- a/e2e/solid-start/basic/src/routeTree.gen.ts
+++ b/e2e/solid-start/basic/src/routeTree.gen.ts
@@ -32,6 +32,7 @@ import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/
 import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
 import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
 import { Route as NotFoundViaLoaderRouteImport } from './routes/not-found/via-loader'
+import { Route as NotFoundViaHeadRouteImport } from './routes/not-found/via-head'
 import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/via-beforeLoad'
 import { Route as ApiUsersRouteImport } from './routes/api/users'
 import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
@@ -165,6 +166,11 @@ const NotFoundViaLoaderRoute = NotFoundViaLoaderRouteImport.update({
   path: '/via-loader',
   getParentRoute: () => NotFoundRouteRoute,
 } as any)
+const NotFoundViaHeadRoute = NotFoundViaHeadRouteImport.update({
+  id: '/via-head',
+  path: '/via-head',
+  getParentRoute: () => NotFoundRouteRoute,
+} as any)
 const NotFoundViaBeforeLoadRoute = NotFoundViaBeforeLoadRouteImport.update({
   id: '/via-beforeLoad',
   path: '/via-beforeLoad',
@@ -266,6 +272,7 @@ export interface FileRoutesByFullPath {
   '/대한민국': typeof Char45824Char54620Char48124Char44397Route
   '/api/users': typeof ApiUsersRouteWithChildren
   '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+  '/not-found/via-head': typeof NotFoundViaHeadRoute
   '/not-found/via-loader': typeof NotFoundViaLoaderRoute
   '/posts/$postId': typeof PostsPostIdRoute
   '/redirect/$target': typeof RedirectTargetRouteWithChildren
@@ -301,6 +308,7 @@ export interface FileRoutesByTo {
   '/대한민국': typeof Char45824Char54620Char48124Char44397Route
   '/api/users': typeof ApiUsersRouteWithChildren
   '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+  '/not-found/via-head': typeof NotFoundViaHeadRoute
   '/not-found/via-loader': typeof NotFoundViaLoaderRoute
   '/posts/$postId': typeof PostsPostIdRoute
   '/search-params/default': typeof SearchParamsDefaultRoute
@@ -342,6 +350,7 @@ export interface FileRoutesById {
   '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
   '/api/users': typeof ApiUsersRouteWithChildren
   '/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+  '/not-found/via-head': typeof NotFoundViaHeadRoute
   '/not-found/via-loader': typeof NotFoundViaLoaderRoute
   '/posts/$postId': typeof PostsPostIdRoute
   '/redirect/$target': typeof RedirectTargetRouteWithChildren
@@ -383,6 +392,7 @@ export interface FileRouteTypes {
     | '/대한민국'
     | '/api/users'
     | '/not-found/via-beforeLoad'
+    | '/not-found/via-head'
     | '/not-found/via-loader'
     | '/posts/$postId'
     | '/redirect/$target'
@@ -418,6 +428,7 @@ export interface FileRouteTypes {
     | '/대한민국'
     | '/api/users'
     | '/not-found/via-beforeLoad'
+    | '/not-found/via-head'
     | '/not-found/via-loader'
     | '/posts/$postId'
     | '/search-params/default'
@@ -458,6 +469,7 @@ export interface FileRouteTypes {
     | '/_layout/_layout-2'
     | '/api/users'
     | '/not-found/via-beforeLoad'
+    | '/not-found/via-head'
     | '/not-found/via-loader'
     | '/posts/$postId'
     | '/redirect/$target'
@@ -668,6 +680,13 @@ declare module '@tanstack/solid-router' {
       preLoaderRoute: typeof NotFoundViaLoaderRouteImport
       parentRoute: typeof NotFoundRouteRoute
     }
+    '/not-found/via-head': {
+      id: '/not-found/via-head'
+      path: '/via-head'
+      fullPath: '/not-found/via-head'
+      preLoaderRoute: typeof NotFoundViaHeadRouteImport
+      parentRoute: typeof NotFoundRouteRoute
+    }
     '/not-found/via-beforeLoad': {
       id: '/not-found/via-beforeLoad'
       path: '/via-beforeLoad'
@@ -785,12 +804,14 @@ declare module '@tanstack/solid-router' {
 
 interface NotFoundRouteRouteChildren {
   NotFoundViaBeforeLoadRoute: typeof NotFoundViaBeforeLoadRoute
+  NotFoundViaHeadRoute: typeof NotFoundViaHeadRoute
   NotFoundViaLoaderRoute: typeof NotFoundViaLoaderRoute
   NotFoundIndexRoute: typeof NotFoundIndexRoute
 }
 
 const NotFoundRouteRouteChildren: NotFoundRouteRouteChildren = {
   NotFoundViaBeforeLoadRoute: NotFoundViaBeforeLoadRoute,
+  NotFoundViaHeadRoute: NotFoundViaHeadRoute,
   NotFoundViaLoaderRoute: NotFoundViaLoaderRoute,
   NotFoundIndexRoute: NotFoundIndexRoute,
 }
diff --git a/e2e/solid-start/basic/src/routes/not-found/index.tsx b/e2e/solid-start/basic/src/routes/not-found/index.tsx
index 34c8ef6146..54eff41247 100644
--- a/e2e/solid-start/basic/src/routes/not-found/index.tsx
+++ b/e2e/solid-start/basic/src/routes/not-found/index.tsx
@@ -25,6 +25,16 @@ export const Route = createFileRoute('/not-found/')({
             via-loader
           </Link>
         </div>
+        <div class="mb-2">
+          <Link
+            from={Route.fullPath}
+            to="./via-head"
+            preload={preload()}
+            data-testid="via-head"
+          >
+            via-head
+          </Link>
+        </div>
       </div>
     )
   },
diff --git a/e2e/solid-start/basic/src/routes/not-found/via-head.tsx b/e2e/solid-start/basic/src/routes/not-found/via-head.tsx
new file mode 100644
index 0000000000..51806db45e
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/not-found/via-head.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, notFound } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/not-found/via-head')({
+  head: () => {
+    throw notFound()
+  },
+  component: RouteComponent,
+  notFoundComponent: () => {
+    return (
+      <div data-testid="via-head-notFound-component">
+        Not Found "/not-found/via-head"!
+      </div>
+    )
+  },
+})
+
+function RouteComponent() {
+  return (
+    <div data-testid="via-head-route-component" data-server={typeof window}>
+      Hello "/not-found/via-head"!
+    </div>
+  )
+}
diff --git a/e2e/solid-start/basic/tests/not-found.spec.ts b/e2e/solid-start/basic/tests/not-found.spec.ts
index e1a69eefc6..1bed8e991c 100644
--- a/e2e/solid-start/basic/tests/not-found.spec.ts
+++ b/e2e/solid-start/basic/tests/not-found.spec.ts
@@ -9,6 +9,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport
 test.use({
   whitelistErrors: [
     /Failed to load resource: the server responded with a status of 404/,
+    'NotFound error during hydration for routeId',
   ],
 })
 test.describe('not-found', () => {
@@ -25,7 +26,7 @@ test.describe('not-found', () => {
   test.describe('throw notFound()', () => {
     const navigationTestMatrix = combinate({
       // TODO beforeLoad!
-      thrower: [/* 'beforeLoad',*/ 'loader'] as const,
+      thrower: [/* 'beforeLoad',*/ 'head', 'loader'] as const,
       preload: [false, true] as const,
     })
 
@@ -56,7 +57,7 @@ test.describe('not-found', () => {
     const directVisitTestMatrix = combinate({
       // TODO beforeLoad!
 
-      thrower: [/* 'beforeLoad',*/ 'loader'] as const,
+      thrower: [/* 'beforeLoad',*/ 'head', 'loader'] as const,
     })
 
     directVisitTestMatrix.forEach(({ thrower }) => {
diff --git a/e2e/solid-start/basic/vite.config.ts b/e2e/solid-start/basic/vite.config.ts
index 6af116d625..7e8d607c53 100644
--- a/e2e/solid-start/basic/vite.config.ts
+++ b/e2e/solid-start/basic/vite.config.ts
@@ -20,6 +20,7 @@ const prerenderConfiguration = {
       '/redirect',
       '/i-do-not-exist',
       '/not-found/via-beforeLoad',
+      '/not-found/via-head',
       '/not-found/via-loader',
       '/search-params/default',
       '/transition',
diff --git a/packages/react-router/tests/hydrate.test.ts b/packages/react-router/tests/hydrate.test.ts
new file mode 100644
index 0000000000..b0d9153055
--- /dev/null
+++ b/packages/react-router/tests/hydrate.test.ts
@@ -0,0 +1,265 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { hydrate } from '@tanstack/router-core/ssr/client'
+import {
+  createMemoryHistory,
+  createRootRoute,
+  createRoute,
+  createRouter,
+  notFound,
+} from '../src'
+import type { TsrSsrGlobal } from '@tanstack/router-core/ssr/client'
+import type { AnyRouteMatch } from '../src'
+
+describe('hydrate', () => {
+  let mockWindow: { $_TSR?: TsrSsrGlobal }
+  let mockRouter: any
+  let mockHead: any
+
+  beforeEach(() => {
+    // Reset global window mock
+    mockWindow = {}
+    ;(global as any).window = mockWindow
+
+    // Reset mock head function
+    mockHead = vi.fn()
+
+    const history = createMemoryHistory({ initialEntries: ['/'] })
+
+    const rootRoute = createRootRoute({})
+
+    const indexRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+      component: () => 'Index',
+      notFoundComponent: () => 'Not Found',
+      head: mockHead,
+    })
+
+    const otherRoute = createRoute({
+      getParentRoute: () => indexRoute,
+      path: '/other',
+      component: () => 'Other',
+    })
+
+    const routeTree = rootRoute.addChildren([
+      indexRoute.addChildren([otherRoute]),
+    ])
+
+    mockRouter = createRouter({ routeTree, history, isServer: true })
+  })
+
+  afterEach(() => {
+    vi.resetAllMocks()
+    delete (global as any).window
+  })
+
+  it('should throw error if window.$_TSR is not available', async () => {
+    await expect(hydrate(mockRouter)).rejects.toThrow(
+      'Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!',
+    )
+  })
+
+  it('should throw error if window.$_TSR.router is not available', async () => {
+    mockWindow.$_TSR = {
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: [],
+      initialized: false,
+      // router is missing
+    } as any
+
+    await expect(hydrate(mockRouter)).rejects.toThrow(
+      'Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',
+    )
+  })
+
+  it('should initialize serialization adapters when provided', async () => {
+    const mockSerializer = {
+      key: 'testAdapter',
+      fromSerializable: vi.fn(),
+      toSerializable: vi.fn(),
+      test: vi.fn().mockReturnValue(true),
+      '~types': {
+        input: {},
+        output: {},
+        extends: {},
+      },
+    }
+
+    mockRouter.options.serializationAdapters = [mockSerializer]
+
+    const mockMatches = [{ id: '/', routeId: '/', index: 0, _nonReactive: {} }]
+    mockRouter.matchRoutes = vi.fn().mockReturnValue(mockMatches)
+    mockRouter.state.matches = mockMatches
+
+    const mockBuffer = [vi.fn(), vi.fn()]
+    mockWindow.$_TSR = {
+      router: {
+        manifest: { routes: {} },
+        dehydratedData: {},
+        lastMatchId: '/',
+        matches: [],
+      },
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: mockBuffer,
+      initialized: false,
+    }
+
+    await hydrate(mockRouter)
+
+    expect(mockWindow.$_TSR.t).toBeInstanceOf(Map)
+    expect(mockWindow.$_TSR.t?.get('testAdapter')).toBe(
+      mockSerializer.fromSerializable,
+    )
+    expect(mockBuffer[0]).toHaveBeenCalled()
+    expect(mockBuffer[1]).toHaveBeenCalled()
+    expect(mockWindow.$_TSR.initialized).toBe(true)
+  })
+
+  it('should handle empty serialization adapters', async () => {
+    mockRouter.options.serializationAdapters = []
+
+    mockWindow.$_TSR = {
+      router: {
+        manifest: { routes: {} },
+        dehydratedData: {},
+        lastMatchId: '/',
+        matches: [],
+      },
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: [],
+      initialized: false,
+    }
+
+    await hydrate(mockRouter)
+
+    expect(mockWindow.$_TSR.t).toBeUndefined()
+    expect(mockWindow.$_TSR.initialized).toBe(true)
+  })
+
+  it('should set manifest in router.ssr', async () => {
+    const testManifest = { routes: {} }
+    mockWindow.$_TSR = {
+      router: {
+        manifest: testManifest,
+        dehydratedData: {},
+        lastMatchId: '/',
+        matches: [],
+      },
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: [],
+      initialized: false,
+    }
+
+    await hydrate(mockRouter)
+
+    expect(mockRouter.ssr).toEqual({
+      manifest: testManifest,
+    })
+  })
+
+  it('should hydrate matches', async () => {
+    const mockMatches = [
+      {
+        id: '/',
+        routeId: '/',
+        index: 0,
+        ssr: undefined,
+        _nonReactive: {},
+      },
+      {
+        id: '/other',
+        routeId: '/other',
+        index: 1,
+        ssr: undefined,
+        _nonReactive: {},
+      },
+    ]
+
+    const dehydratedMatches = [
+      {
+        i: '/',
+        l: { indexData: 'server-data' },
+        s: 'success' as const,
+        ssr: true,
+        u: Date.now(),
+      },
+    ]
+
+    mockRouter.matchRoutes = vi.fn().mockReturnValue(mockMatches)
+    mockRouter.state.matches = mockMatches
+
+    mockWindow.$_TSR = {
+      router: {
+        manifest: { routes: {} },
+        dehydratedData: {},
+        lastMatchId: '/',
+        matches: dehydratedMatches,
+      },
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: [],
+      initialized: false,
+    }
+
+    await hydrate(mockRouter)
+
+    const { id, loaderData, ssr, status } = mockMatches[0] as AnyRouteMatch
+    expect(id).toBe('/')
+    expect(loaderData).toEqual({ indexData: 'server-data' })
+    expect(status).toBe('success')
+    expect(ssr).toBe(true)
+  })
+
+  it('should handle errors during route context hydration', async () => {
+    const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+    mockHead.mockImplementation(() => {
+      throw notFound()
+    })
+
+    const mockMatches = [
+      { id: '/', routeId: '/', index: 0, ssr: true, _nonReactive: {} },
+    ]
+
+    mockRouter.matchRoutes = vi.fn().mockReturnValue(mockMatches)
+    mockRouter.state.matches = mockMatches
+
+    mockWindow.$_TSR = {
+      router: {
+        manifest: { routes: {} },
+        dehydratedData: {},
+        lastMatchId: '/',
+        matches: [
+          {
+            i: '/',
+            l: { data: 'test' },
+            s: 'success',
+            ssr: true,
+            u: Date.now(),
+          },
+        ],
+      },
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: [],
+      initialized: false,
+    }
+
+    await hydrate(mockRouter)
+
+    const match = mockRouter.state.matches[0] as AnyRouteMatch
+    expect(match.error).toEqual({ isNotFound: true })
+
+    expect(consoleSpy).toHaveBeenCalledWith(
+      'NotFound error during hydration for routeId: /',
+      expect.objectContaining({
+        isNotFound: true,
+      }),
+    )
+
+    consoleSpy.mockRestore()
+  })
+})
diff --git a/packages/router-core/src/ssr/ssr-client.ts b/packages/router-core/src/ssr/ssr-client.ts
index 18e9f394bb..4f52ca66a2 100644
--- a/packages/router-core/src/ssr/ssr-client.ts
+++ b/packages/router-core/src/ssr/ssr-client.ts
@@ -1,5 +1,6 @@
 import invariant from 'tiny-invariant'
 import { batch } from '@tanstack/store'
+import { isNotFound } from '../not-found'
 import { createControlledPromise } from '../utils'
 import type { AnyRouteMatch, MakeRouteMatch } from '../Matches'
 import type { AnyRouter } from '../router'
@@ -179,52 +180,72 @@ export async function hydrate(router: AnyRouter): Promise<any> {
   // 2) execute `head()` and `scripts()` for each match
   await Promise.all(
     router.state.matches.map(async (match) => {
-      const route = router.looseRoutesById[match.routeId]!
+      try {
+        const route = router.looseRoutesById[match.routeId]!
+
+        const parentMatch = router.state.matches[match.index - 1]
+        const parentContext = parentMatch?.context ?? router.options.context
+
+        // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed
+        // so run it again and merge route context
+        if (route.options.context) {
+          const contextFnContext: RouteContextOptions<any, any, any, any> = {
+            deps: match.loaderDeps,
+            params: match.params,
+            context: parentContext ?? {},
+            location: router.state.location,
+            navigate: (opts: any) =>
+              router.navigate({
+                ...opts,
+                _fromLocation: router.state.location,
+              }),
+            buildLocation: router.buildLocation,
+            cause: match.cause,
+            abortController: match.abortController,
+            preload: false,
+            matches,
+          }
+          match.__routeContext =
+            route.options.context(contextFnContext) ?? undefined
+        }
 
-      const parentMatch = router.state.matches[match.index - 1]
-      const parentContext = parentMatch?.context ?? router.options.context
+        match.context = {
+          ...parentContext,
+          ...match.__routeContext,
+          ...match.__beforeLoadContext,
+        }
 
-      // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed
-      // so run it again and merge route context
-      if (route.options.context) {
-        const contextFnContext: RouteContextOptions<any, any, any, any> = {
-          deps: match.loaderDeps,
+        const assetContext = {
+          matches: router.state.matches,
+          match,
           params: match.params,
-          context: parentContext ?? {},
-          location: router.state.location,
-          navigate: (opts: any) =>
-            router.navigate({ ...opts, _fromLocation: router.state.location }),
-          buildLocation: router.buildLocation,
-          cause: match.cause,
-          abortController: match.abortController,
-          preload: false,
-          matches,
+          loaderData: match.loaderData,
+        }
+        const headFnContent = await route.options.head?.(assetContext)
+
+        const scripts = await route.options.scripts?.(assetContext)
+
+        match.meta = headFnContent?.meta
+        match.links = headFnContent?.links
+        match.headScripts = headFnContent?.scripts
+        match.styles = headFnContent?.styles
+        match.scripts = scripts
+      } catch (err) {
+        if (isNotFound(err)) {
+          match.error = { isNotFound: true }
+          console.error(
+            `NotFound error during hydration for routeId: ${match.routeId}`,
+            err,
+          )
+        } else {
+          match.error = err as any
+          console.error(
+            `Error during hydration for route ${match.routeId}:`,
+            err,
+          )
+          throw err
         }
-        match.__routeContext =
-          route.options.context(contextFnContext) ?? undefined
-      }
-
-      match.context = {
-        ...parentContext,
-        ...match.__routeContext,
-        ...match.__beforeLoadContext,
-      }
-
-      const assetContext = {
-        matches: router.state.matches,
-        match,
-        params: match.params,
-        loaderData: match.loaderData,
       }
-      const headFnContent = await route.options.head?.(assetContext)
-
-      const scripts = await route.options.scripts?.(assetContext)
-
-      match.meta = headFnContent?.meta
-      match.links = headFnContent?.links
-      match.headScripts = headFnContent?.scripts
-      match.styles = headFnContent?.styles
-      match.scripts = scripts
     }),
   )
 
diff --git a/packages/solid-router/tests/hydrate.test.ts b/packages/solid-router/tests/hydrate.test.ts
new file mode 100644
index 0000000000..d71e29d7e9
--- /dev/null
+++ b/packages/solid-router/tests/hydrate.test.ts
@@ -0,0 +1,265 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { hydrate } from '@tanstack/router-core/ssr/client'
+import {
+  createMemoryHistory,
+  createRootRoute,
+  createRoute,
+  createRouter,
+  notFound,
+} from '../src'
+import type { AnyRouteMatch } from '../src'
+import type { TsrSsrGlobal } from '@tanstack/router-core/ssr/client'
+
+describe('hydrate', () => {
+  let mockWindow: { $_TSR?: TsrSsrGlobal }
+  let mockRouter: any
+  let mockHead: any
+
+  beforeEach(() => {
+    // Reset global window mock
+    mockWindow = {}
+    ;(global as any).window = mockWindow
+
+    // Reset mock head function
+    mockHead = vi.fn()
+
+    const history = createMemoryHistory({ initialEntries: ['/'] })
+
+    const rootRoute = createRootRoute({})
+
+    const indexRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+      component: () => 'Index',
+      notFoundComponent: () => 'Not Found',
+      head: mockHead,
+    })
+
+    const otherRoute = createRoute({
+      getParentRoute: () => indexRoute,
+      path: '/other',
+      component: () => 'Other',
+    })
+
+    const routeTree = rootRoute.addChildren([
+      indexRoute.addChildren([otherRoute]),
+    ])
+
+    mockRouter = createRouter({ routeTree, history, isServer: true })
+  })
+
+  afterEach(() => {
+    vi.resetAllMocks()
+    delete (global as any).window
+  })
+
+  it('should throw error if window.$_TSR is not available', async () => {
+    await expect(hydrate(mockRouter)).rejects.toThrow(
+      'Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!',
+    )
+  })
+
+  it('should throw error if window.$_TSR.router is not available', async () => {
+    mockWindow.$_TSR = {
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: [],
+      initialized: false,
+      // router is missing
+    } as any
+
+    await expect(hydrate(mockRouter)).rejects.toThrow(
+      'Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',
+    )
+  })
+
+  it('should initialize serialization adapters when provided', async () => {
+    const mockSerializer = {
+      key: 'testAdapter',
+      fromSerializable: vi.fn(),
+      toSerializable: vi.fn(),
+      test: vi.fn().mockReturnValue(true),
+      '~types': {
+        input: {},
+        output: {},
+        extends: {},
+      },
+    }
+
+    mockRouter.options.serializationAdapters = [mockSerializer]
+
+    const mockMatches = [{ id: '/', routeId: '/', index: 0, _nonReactive: {} }]
+    mockRouter.matchRoutes = vi.fn().mockReturnValue(mockMatches)
+    mockRouter.state.matches = mockMatches
+
+    const mockBuffer = [vi.fn(), vi.fn()]
+    mockWindow.$_TSR = {
+      router: {
+        manifest: { routes: {} },
+        dehydratedData: {},
+        lastMatchId: '/',
+        matches: [],
+      },
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: mockBuffer,
+      initialized: false,
+    }
+
+    await hydrate(mockRouter)
+
+    expect(mockWindow.$_TSR.t).toBeInstanceOf(Map)
+    expect(mockWindow.$_TSR.t?.get('testAdapter')).toBe(
+      mockSerializer.fromSerializable,
+    )
+    expect(mockBuffer[0]).toHaveBeenCalled()
+    expect(mockBuffer[1]).toHaveBeenCalled()
+    expect(mockWindow.$_TSR.initialized).toBe(true)
+  })
+
+  it('should handle empty serialization adapters', async () => {
+    mockRouter.options.serializationAdapters = []
+
+    mockWindow.$_TSR = {
+      router: {
+        manifest: { routes: {} },
+        dehydratedData: {},
+        lastMatchId: '/',
+        matches: [],
+      },
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: [],
+      initialized: false,
+    }
+
+    await hydrate(mockRouter)
+
+    expect(mockWindow.$_TSR.t).toBeUndefined()
+    expect(mockWindow.$_TSR.initialized).toBe(true)
+  })
+
+  it('should set manifest in router.ssr', async () => {
+    const testManifest = { routes: {} }
+    mockWindow.$_TSR = {
+      router: {
+        manifest: testManifest,
+        dehydratedData: {},
+        lastMatchId: '/',
+        matches: [],
+      },
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: [],
+      initialized: false,
+    }
+
+    await hydrate(mockRouter)
+
+    expect(mockRouter.ssr).toEqual({
+      manifest: testManifest,
+    })
+  })
+
+  it('should hydrate matches', async () => {
+    const mockMatches = [
+      {
+        id: '/',
+        routeId: '/',
+        index: 0,
+        ssr: undefined,
+        _nonReactive: {},
+      },
+      {
+        id: '/other',
+        routeId: '/other',
+        index: 1,
+        ssr: undefined,
+        _nonReactive: {},
+      },
+    ]
+
+    const dehydratedMatches = [
+      {
+        i: '/',
+        l: { indexData: 'server-data' },
+        s: 'success' as const,
+        ssr: true,
+        u: Date.now(),
+      },
+    ]
+
+    mockRouter.matchRoutes = vi.fn().mockReturnValue(mockMatches)
+    mockRouter.state.matches = mockMatches
+
+    mockWindow.$_TSR = {
+      router: {
+        manifest: { routes: {} },
+        dehydratedData: {},
+        lastMatchId: '/',
+        matches: dehydratedMatches,
+      },
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: [],
+      initialized: false,
+    }
+
+    await hydrate(mockRouter)
+
+    const { id, loaderData, ssr, status } = mockMatches[0] as AnyRouteMatch
+    expect(id).toBe('/')
+    expect(loaderData).toEqual({ indexData: 'server-data' })
+    expect(status).toBe('success')
+    expect(ssr).toBe(true)
+  })
+
+  it('should handle errors during route context hydration', async () => {
+    const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+    mockHead.mockImplementation(() => {
+      throw notFound()
+    })
+
+    const mockMatches = [
+      { id: '/', routeId: '/', index: 0, ssr: true, _nonReactive: {} },
+    ]
+
+    mockRouter.matchRoutes = vi.fn().mockReturnValue(mockMatches)
+    mockRouter.state.matches = mockMatches
+
+    mockWindow.$_TSR = {
+      router: {
+        manifest: { routes: {} },
+        dehydratedData: {},
+        lastMatchId: '/',
+        matches: [
+          {
+            i: '/',
+            l: { data: 'test' },
+            s: 'success',
+            ssr: true,
+            u: Date.now(),
+          },
+        ],
+      },
+      c: vi.fn(),
+      p: vi.fn(),
+      buffer: [],
+      initialized: false,
+    }
+
+    await hydrate(mockRouter)
+
+    const match = mockRouter.state.matches[0] as AnyRouteMatch
+    expect(match.error).toEqual({ isNotFound: true })
+
+    expect(consoleSpy).toHaveBeenCalledWith(
+      'NotFound error during hydration for routeId: /',
+      expect.objectContaining({
+        isNotFound: true,
+      }),
+    )
+
+    consoleSpy.mockRestore()
+  })
+})
