Reading Context from Ancestors

In the previous article, we learned how to provide context—how to broadcast data from an ancestor component so descendants can receive it. Now we turn to the other side of the equation: consuming context.

Providing context is straightforward: call setContext with a key and value, done. Consuming it has more nuance. What happens when context doesn’t exist? How do you handle optional versus required context? What about type safety? And there’s a subtle reactivity trap with destructuring that catches many developers off guard.

This article is intentionally thorough

You don’t need to absorb everything on the first read — focus on the patterns and revisit as needed. This guide serves both as a learning resource and a reference you’ll return to.

This article covers everything you need to consume context confidently:

  • The three consumer functions: getContext, hasContext, and getAllContexts
  • Handling missing context: fail-fast, fallbacks, and defensive patterns
  • Building reusable utilities that standardize context access
  • Type-safe consumption with createContext and typed helpers
  • The destructuring trap that breaks reactivity (and how to avoid it)

The getContext Function

The getContext function is your primary tool for receiving context values. Let’s examine it thoroughly.

Function Signature

function getContext<T>(key: any): T

The function takes a single parameter, the key that identifies which context you want, and returns the context value. The key must exactly match what was used in setContext.

Basic Usage

At its simplest:

<script>
	import { getContext } from 'svelte'

	const user = getContext('user')
</script>

<p>Welcome, {user.name}!</p>

If an ancestor component called setContext('user', { name: 'Alice' }), then getContext('user') returns { name: 'Alice' }.

The Lookup Process

When you call getContext(key), Svelte performs a lookup that walks up the component tree:

  1. Start at the current component’s parent — Context is set on components, but you can only access context from ancestors, not from yourself.

  2. Check each ancestor — Svelte checks if that ancestor called setContext(key, ...).

  3. Return on first match — As soon as an ancestor with that key is found, its value is returned.

  4. Return undefined if not found — If no ancestor set that key, returned value is undefined and not an error so be careful.

This process is visualized below:

App (setContext 'user' → Alice)    ← Step 4: Found! Return Alice
└── Layout                          ← Step 3: No 'user' context
    └── Dashboard                   ← Step 2: No 'user' context
        └── UserBadge               ← Step 1: getContext('user') called here
            Looking for 'user'...

IMPORTANT: This lookup happens only once during component initialization — context is structural, not reactive by design.

The “Nearest Wins” Rule

If multiple ancestors set the same key, you get the value from the nearest (most immediate) ancestor:

RootLayout -> (setContext 'theme' → 'light')
└── Dashboard
    └── AdminPanel -> (setContext 'theme' → 'dark')
        └── Settings
            └── ThemeDisplay  ← getContext('theme') returns 'dark'
ThemeDisplay
↑ SettingsPanel      (no theme)
↑ DashboardLayout    ← FOUND: 'dark'
↑ RootLayout         (ignored)

The ThemeDisplay component in Settings page gets 'dark' because AdminPanel is closer than RootLayout. This enables powerful override patterns where inner components can customize context for their subtree without affecting the rest of the application,

What getContext Returns

The return value is exactly what was passed to setContext. No transformation, no copying, no proxy — just the same value.

For primitives (strings, numbers, booleans … ), you get the value as it was at the time setContext was called:

<!-- Provider -->
<script>
	import { setContext } from 'svelte'
	let count = 5
	setContext('count', count)  // Stores 5
	count = 100  // Doesn't affect context
</script>

<!-- Consumer -->
<script>
	import { getContext } from 'svelte'
	const count = getContext('count')  // Gets 5, not 100
</script>

For objects (including arrays, functions, class instances), you get a reference to the same object:

<!-- Provider -->
<script>
	import { setContext } from 'svelte'
	const user = { name: 'Alice', score: 0 }
	setContext('user', user)
	// Later...
	user.score = 100  // This IS visible to consumers
</script>

<!-- Consumer -->
<script>
	import { getContext } from 'svelte'
	const user = getContext('user')
	console.log(user.score)  // 100 (sees the modification)
</script>

This reference sharing is the foundation of reactive context patterns — but it requires care. We’ll explore this in depth in the Making Context Reactive article.

Missing Context Returns - undefined

A critical behavior: getContext does not throw Error when the key isn’t found. It silently returns undefined:

<script>
	import { getContext } from 'svelte'

	const user = getContext('user') // undefined if no ancestor set 'user'

	// This would crash!
	console.log(user.name) // TypeError: Cannot read property 'name' of undefined
</script>

Here’s what happens when context is missing:

┌──────────────────────────────────────────────────────────────────┐
│ Component Initialization                                         │
│                                                                  │
│ const user = getContext('user')  // No provider found            │
│                    ↓                                             │
│           Returns undefined ⚠️  (No error thrown!)               │
│                    ↓                                             │
│ Execution continues normally...                                  │
└──────────────────────────────────────────────────────────────────┘

         ↓ Later, when code tries to use it...
┌──────────────────────────────────────────────────────────────────┐
│ Event Handler / Effect / Render                                  │
│                                                                  │
│ function handleLogin() {                                         │
│   user.login(credentials)  ❌ TypeError: undefined.login         │
│ }                                                                │
│                                                                  │
│ <h1>{user.name}</h1>  ❌ TypeError: undefined.name               │
└──────────────────────────────────────────────────────────────────┘

Problem: Error occurs far from root cause!


Three Prevention Strategies:

┌─────────────────────┐   ┌─────────────────────┐   ┌──────────────────────┐
│ 1. FAIL FAST        │   │ 2. FALLBACK         │   │ 3. NULLABLE          │
│                     │   │                     │   │                      │
│ if (!hasContext())  │   │ const user =        │   │ const user =         │
│   throw Error       │   │   hasContext()      │   │   hasContext()       │
│                     │   │   ? getContext()    │   │   ? getContext()     │
│ ✅ Detect at init   │   │   : defaultUser     │   │   : null             │
│ ✅ Clear message    │   │                     │   │                      │
│ ✅ Can't be ignored │   │ ✅ Always usable    │   │ if (user) {          │
│                     │   │ ✅ No crash         │   │   // Has context     │
│ Use: Required       │   │                     │   │ } else {             │
│      context        │   │ Use: Optional       │   │   // No context      │
│                     │   │      enhancement    │   │ }                    │
│                     │   │                     │   │                      │
│                     │   │                     │   │ ✅ Explicit check    │
│                     │   │                     │   │ ✅ Branch logic      │
│                     │   │                     │   │                      │
│                     │   │                     │   │ Use: Conditional     │
│                     │   │                     │   │      features        │
└─────────────────────┘   └─────────────────────┘   └──────────────────────┘

This behavior is intentional — it allows optional context patterns — but requires careful handling. The next section covers when and how to use each strategy.


The Timing Constraint: When getContext Can Be Called

Like setContext, getContext must be called during component initialization — the synchronous execution of your <script> block.

Component Lifecycle Timeline
─────────────────────────────────────────────────────────────────────

Phase 1: INITIALIZATION                Phase 2: REACTIVE RUNTIME
┌─────────────────────────┐           ┌──────────────────────────┐
│ ✅ getContext ALLOWED   │           │ ❌ getContext FORBIDDEN  │
│                         │           │                          │
│ <script>                │           │ • Event handlers         │
│   const x = getContext()│           │ • $effect() callbacks    │
│                         │           │ • setTimeout/Promise     │
│   function setup() {    │           │ • After await            │
│     const y = getContext│           │                          │
│   }                     │           │ function onClick() {     │
│   setup() // ✅ Called  │           │   getContext() // ❌     │
│         immediately     │           │ }                        │
│ </script>               │           │                          │
│                         │           │ $effect(() => {          │
│ All synchronous code    │           │   getContext() // ❌     │
│ in <script> block runs  │           │ })                       │
└─────────────────────────┘           └──────────────────────────┘
         │                                       │
         │ Component mounted                     │ Reactive updates
         ↓                                       ↓
    [Context captured]                    [Use captured values]

Key insight: Capture context references early, use them throughout. If the context value contains $state properties, those remain reactive even though the lookup itself was one-time.

Valid Locations

the following are valid places to call getContext:

<script>
	import { getContext } from 'svelte'

	// ✅ Top-level in script block
	const theme = getContext('theme')

	// ✅ Inside synchronous function called immediately
	function setupFromContext() {
		const config = getContext('config')
		return config.apiUrl
	}
	const apiUrl = setupFromContext() // Called during initialization

	// ✅ Inside IIFE (Immediately Invoked Function Expression)
	const user = (() => {
		return getContext('user')
	})()

	// ✅ Inside synchronous conditional
	let debug = false
	if (import.meta.env.DEV) {
		const devTools = getContext('devTools')
		debug = devTools?.enabled ?? false
	}
</script>

Invalid Locations

do not call getContext in these places:

<script>
	import { getContext } from 'svelte'

	// ❌ Inside event handler
	function handleClick() {
		const user = getContext('user') // Error!
	}

	// ❌ Inside $effect
	$effect(() => {
		const theme = getContext('theme') // Error!
	})

	// ❌ Inside $derived (which runs reactively)
	// Note: $derived runs during initialization, but subsequent runs are reactive
	// This is a subtle edge case—generally avoid getContext in $derived

	// ❌ Inside setTimeout
	setTimeout(() => {
		const config = getContext('config') // Error!
	}, 0)

	// ❌ Inside Promise callbacks
	fetch('/api').then(() => {
		const api = getContext('api') // Error!
	})

	// ❌ After await (which makes the rest async)
	async function setup() {
		const data = await fetch('/api').then((r) => r.json())
		const config = getContext('config') // Error!
	}
	setup()
</script>

The Error Message

If you call getContext outside initialization, Svelte throws:

Error: `getContext(...)` can only be called during component initialization

Why This Restriction Exists

This restriction mirrors the one on setContext, and for related reasons:

  1. Context is structural — It’s part of how the component tree is organized, established at mount time.

  2. Predictability — You can rely on context being available (or not) from the moment the component initializes. It won’t suddenly appear or disappear.

  3. Performance — Svelte doesn’t need to track context access reactively; it’s a one-time lookup during initialization.

The Workaround: Capture During Initialization

The solution is simple: capture context values during initialization, then use them wherever you need:

<script>
	import { getContext } from 'svelte'

	// Capture during initialization
	const auth = getContext('auth')
	const theme = getContext('theme')
	const toast = getContext('toast')

	// Now use freely in event handlers
	function handleLogin() {
		auth
			.login(credentials)
			.then(() => {
				toast.success('Welcome back!')
			})
			.catch((err) => {
				toast.error(err.message)
			})
	}

	// And in effects
	$effect(() => {
		document.body.setAttribute('data-theme', theme.current)
	})

	// And in derived values
	let greeting = $derived(`Hello, ${auth.user?.name ?? 'Guest'}!`)
</script>

<button onclick={handleLogin}>Login</button><p>{greeting}</p>

The captured reference remains valid for the component’s lifetime. If the context value is an object with reactive properties (using $state), those reactive properties will update normally.


Checking Context with hasContext

Before accessing context, you might want to check if it exists. The hasContext function does exactly this.

Function Signature

function hasContext(key: any): boolean

Returns true if any ancestor set this context key, false otherwise.

Basic Usage

<script>
	import { getContext, hasContext } from 'svelte'

	// Check before accessing
	if (hasContext('user')) {
		const user = getContext('user')
		console.log('User:', user.name)
	} else {
		console.log('No user context available')
	}
</script>

Common Pattern: Check and Provide Default

<script>
	import { getContext, hasContext } from 'svelte'

	// Get context with fallback
	const theme = hasContext('theme')
		? getContext('theme')
		: { mode: 'light', accentColor: '#007bff' }
</script>

This pattern is so common that you might want to create a utility function (we’ll do that later).

When to Use hasContext

The decision of whether to check for context depends on your use case:

ScenarioApproachReasoning
Context is requiredDon’t check; let it failMissing required context is a bug; fail fast and loudly
Context is optional with sensible defaultCheck with hasContext, provide defaultComponent should work standalone
Different behavior with/without contextCheck and branchSome features only available in certain contexts
Library/reusable componentUsually checkComponents might be used outside expected providers
Internal application componentOften don’t checkYou control the environment

hasContext Doesn’t Consume Context

An important detail: hasContext only checks existence—it doesn’t retrieve or “consume” the context. You can call it multiple times without side effects:

<script>
	import { hasContext, getContext } from 'svelte'

	// These are independent checks
	const hasTheme = hasContext('theme')
	const hasUser = hasContext('user')
	const hasFeatureFlags = hasContext('features')

	// Now conditionally get what we need
	const theme = hasTheme ? getContext('theme') : defaultTheme
	const user = hasUser ? getContext('user') : null
</script>

Inspecting All Context with getAllContexts

The getAllContexts function returns the complete context map available at the current position in the tree.

Function Signature

function getAllContexts<T extends Map<any, any> = Map<any, any>>(): T

Returns a JavaScript Map containing all context key-value pairs accessible to this component.

Basic Usage

<script>
	import { getAllContexts } from 'svelte'

	const allContexts = getAllContexts()

	console.log(allContexts)
	// Map(3) {
	//   'theme' => { mode: 'dark', ... },
	//   'user' => { name: 'Alice', ... },
	//   Symbol('auth') => { ... }
	// }

	// Use Map methods
	console.log(allContexts.has('theme')) // true
	console.log(allContexts.get('user')) // { name: 'Alice', ... }
	console.log(allContexts.size) // 3
</script>

1: Debugging

When something isn’t working, it’s helpful to see what context is actually available:

<script>
	import { getAllContexts } from 'svelte'

	// Development-only context inspection
	if (import.meta.env.DEV) {
		const ctx = getAllContexts()
		console.group('🔍 Available Context')
		for (const [key, value] of ctx) {
			console.log(`${String(key)}:`, value)
		}
		console.groupEnd()
	}
</script>

You might create a reusable debug component:

<!-- ContextDebugger.svelte -->
<script>
	import { getAllContexts } from 'svelte'

	let { show = false } = $props()

	const allContexts = getAllContexts()
	const entries = [...allContexts.entries()]
</script>

{#if show && import.meta.env.DEV}
	<details class="context-debugger">
		<summary>Context ({entries.length} keys)</summary>
		<ul>
			{#each entries as [key, value]}
				<li>
					<strong>{String(key)}</strong>
					<pre>{JSON.stringify(value, null, 2)}</pre>
				</li>
			{/each}
		</ul>
	</details>
{/if}

<style>
	.context-debugger {
		position: fixed;
		bottom: 1rem;
		right: 1rem;
		background: #1e1e1e;
		color: #d4d4d4;
		padding: 0.5rem;
		border-radius: 4px;
		font-size: 12px;
		max-width: 400px;
		max-height: 300px;
		overflow: auto;
		z-index: 9999;
	}

	pre {
		margin: 0;
		white-space: pre-wrap;
		word-break: break-all;
	}
</style>

2: Forwarding Context to Dynamic Components

When you dynamically mount a component outside the normal tree (e.g., a modal rendered in a portal), you may want it to inherit the current context:

<script>
	import { mount, getAllContexts } from 'svelte'
	import Modal from './Modal.svelte'

	// Capture context that should be forwarded
	const inheritedContext = getAllContexts()

	let modalInstance = null

	function openModal(content) {
		// Mount modal at document.body but with inherited context
		modalInstance = mount(Modal, {
			target: document.body,
			props: { content },
			context: inheritedContext // Forward context!
		})
	}

	function closeModal() {
		if (modalInstance) {
			modalInstance.$destroy()
			modalInstance = null
		}
	}
</script>

Without forwarding context, the modal would have no context at all (since document.body has no Svelte ancestor with context).

3: Creating Context-Forwarding Wrappers

Sometimes you need to wrap a component while preserving all its context access:

<!-- ContextPreservingWrapper.svelte -->
<script>
	import { getAllContexts, setContext } from 'svelte'

	let { children, additionalContext = {} } = $props()

	// Get all inherited context
	const inherited = getAllContexts()

	// Re-provide everything from ancestors
	for (const [key, value] of inherited) {
		setContext(key, value)
	}

	// Add any additional context
	for (const [key, value] of Object.entries(additionalContext)) {
		setContext(key, value)
	}
</script>

<div class="wrapper">
	{@render children()}
</div>

Timing Note

Like the other context functions, getAllContexts must be called during component initialization.


Handling Missing Context: Strategies and Patterns

Missing context is one of the most common sources of runtime errors in context-based architectures. A component expects getContext('user') to return a user object, but instead gets undefined—and suddenly you’re debugging a cryptic “Cannot read property ‘name’ of undefined” error three components away from the actual problem.

The key insight is that different situations call for different strategies. Sometimes missing context is a bug that should crash loudly. Other times it’s expected and your component should gracefully degrade. Let’s explore when to use each approach.

Strategy 1: Fail Fast for Required Context

When context is genuinely required—meaning your component cannot function without it—the best approach is to fail immediately with a clear, actionable error message:

<script>
	import { getContext, hasContext } from 'svelte'

	if (!hasContext('auth')) {
		throw new Error(
			'AuthContext not found. ' +
				'Wrap this component in a layout that provides auth context. ' +
				'See: src/routes/+layout.svelte'
		)
	}

	const auth = getContext('auth')
</script>

Why fail fast?

  • Immediate feedback: You discover the problem during development, not in production when a user clicks a button
  • Clear diagnosis: The error message tells developers exactly what’s wrong and where to look
  • Prevents cascading failures: Without this check, you’d get a confusing error like TypeError: Cannot read property 'login' of undefined somewhere deep in an event handler

Use this strategy for contexts that are architectural requirements—things like authentication, routing, or app-wide configuration.

Strategy 2: Graceful Fallback for Optional Context

Some context enhances a component but isn’t essential. A Button component might look better when it respects theme context, but it should still render a perfectly functional button without it:

<script>
	import { getContext, hasContext } from 'svelte'

	let { children, onclick } = $props()

	// Theme is optional—component works without it
	const defaultTheme = {
		primaryColor: '#007bff',
		borderRadius: '4px'
	}

	const theme = hasContext('theme') ? getContext('theme') : defaultTheme
</script>

<button
	{onclick}
	style="
		background: {theme.primaryColor};
		border-radius: {theme.borderRadius};
	"
>
	{@render children()}
</button>

This pattern makes components portable. You can drop them into any project—whether it has your theme system or not—and they just work.

Strategy 3: Explicit Null for Conditional Features

Sometimes you need to know whether context was provided versus using a default. For example, you might show different UI for logged-in versus anonymous users:

<script>
	import { getContext, hasContext } from 'svelte'

	// Explicitly null when not available (not a default user object)
	const user = hasContext('user') ? getContext('user') : null
</script>

{#if user}
	<nav>
		<span>Welcome, {user.name}</span>
		<a href="/dashboard">Dashboard</a>
		<button onclick={() => auth.logout()}>Logout</button>
	</nav>
{:else}
	<nav>
		<a href="/login">Sign In</a>
		<a href="/register">Create Account</a>
	</nav>
{/if}

Using null (rather than a fake default user) makes the distinction crystal clear in your code.

Strategy 4: Optional Chaining for Quick Access

For simple cases where you just need safe property access, optional chaining is concise:

<script>
	import { getContext } from 'svelte'

	const user = getContext('user')
	const analytics = getContext('analytics')
</script>

<!-- Safe even if user is undefined -->
<p>Hello, {user?.name ?? 'Guest'}!</p>

<!-- Conditionally call methods -->
<button onclick={() => analytics?.track('button_clicked')}> Click me </button>

This works well for leaf components that consume context passively. However, avoid overusing it—if you find yourself writing user?. everywhere, consider using Strategy 2 or 3 at the top of your script instead.

Choosing the Right Strategy

ScenarioStrategyWhy
Component cannot function without contextFail FastCatch config errors immediately
Context enhances but isn’t requiredGraceful FallbackComponent stays portable
Different UI for “has context” vs “doesn’t”Explicit NullClear conditional logic
One-off property accessOptional ChainingConcise for simple cases
Library/reusable componentGraceful FallbackWorks in any environment
Internal app component with guaranteed providerFail FastDocuments the requirement

Building Reusable Context Utilities

If you find yourself repeating the same context-handling patterns across many components, it’s time to extract them into utilities. This not only reduces boilerplate but also standardizes how your team handles context throughout the application.

A Practical Utility Module

Here’s a set of utilities that cover the most common patterns:

// src/lib/context/utils.ts
import { getContext, hasContext } from 'svelte'

/**
 * Gets context or returns a fallback value.
 * Use for optional context with sensible defaults.
 *
 * @example
 * const theme = getContextOr('theme', { mode: 'light' })
 */
export function getContextOr<T>(key: any, fallback: T): T {
	return hasContext(key) ? getContext(key) : fallback
}

/**
 * Gets context or returns null.
 * Use when you need to distinguish "no context" from "has context".
 *
 * @example
 * const user = getContextOrNull('user')
 * if (user) { ... }
 */
export function getContextOrNull<T>(key: any): T | null {
	return hasContext(key) ? getContext(key) : null
}

/**
 * Gets context or throws a descriptive error.
 * Use for required context that must be provided by an ancestor.
 *
 * @example
 * const auth = requireContext('auth', 'Authentication')
 */
export function requireContext<T>(key: any, contextName: string): T {
	if (!hasContext(key)) {
		throw new Error(
			`${contextName} context is required but was not found. ` +
				`Ensure this component is rendered within a provider that sets '${String(key)}' context.`
		)
	}
	return getContext(key)
}

Using the Utilities

With these utilities, your components become cleaner and more consistent:

<script>
	import { requireContext, getContextOr, getContextOrNull } from '$lib/context/utils'

	// Required: throws descriptive error if missing
	const auth = requireContext('auth', 'Authentication')

	// Optional with default: always returns a usable value
	const theme = getContextOr('theme', { mode: 'light', accent: '#007bff' })

	// Optional nullable: returns null if missing
	const analytics = getContextOrNull('analytics')

	function trackClick() {
		// Only track if analytics is available
		analytics?.track('button_clicked')
	}
</script>

Creating Typed Context Accessors

For even better developer experience, create typed accessors for each context in your app:

// src/lib/context/auth.ts
import { getContext, hasContext, setContext } from 'svelte'

export interface AuthContext {
	readonly user: User | null
	readonly isAuthenticated: boolean
	login(credentials: Credentials): Promise<void>
	logout(): Promise<void>
}

const AUTH_KEY = Symbol('auth')

export function setAuthContext(value: AuthContext) {
	return setContext(AUTH_KEY, value)
}

export function getAuthContext(): AuthContext {
	if (!hasContext(AUTH_KEY)) {
		throw new Error(
			'Auth context not found. ' +
				'Ensure this component is rendered within src/routes/+layout.svelte'
		)
	}
	return getContext(AUTH_KEY)
}

export function getAuthContextOrNull(): AuthContext | null {
	return hasContext(AUTH_KEY) ? getContext(AUTH_KEY) : null
}

Now consuming auth context is fully typed and self-documenting:

<script lang="ts">
	import { getAuthContext } from '$lib/context/auth'

	const auth = getAuthContext()

	// Full TypeScript support
	auth.user?.name // ✅ string | undefined
	auth.isAuthenticated // ✅ boolean
	auth.login(creds) // ✅ Promise<void>
</script>

This pattern scales well—create one file per context domain (auth.ts, theme.ts, toast.ts) and your team always knows exactly how to provide and consume each context.


Type-Safe Consumption

TypeScript integration makes consuming context safer and more ergonomic.

Basic Type Assertions

With raw getContext, you need to assert types:

<script lang="ts">
	import { getContext } from 'svelte'

	interface User {
		id: string
		name: string
		email: string
	}

	// Type assertion needed
	const user = getContext('user') as User

	// Or with generic
	const user2 = getContext<User>('user')
</script>

This works but has drawbacks:

  • The assertion could be wrong (no runtime verification)
  • You need to import/define the type everywhere you consume

With createContext (Svelte 5.40+), types are inferred automatically:

// lib/context/user.ts
import { createContext } from 'svelte'

export interface User {
	id: string
	name: string
	email: string
	role: 'admin' | 'user' | 'guest'
}

export const [getUser, setUser] = createContext<User>()
<!-- Consumer.svelte -->
<script lang="ts">
	import { getUser } from '$lib/context/user'

	// Fully typed! No assertion needed.
	const user = getUser()

	// TypeScript knows everything
	user.name // ✅ string
	user.email // ✅ string
	user.role // ✅ 'admin' | 'user' | 'guest'
	user.foo // ❌ Property 'foo' does not exist
</script>

createContext Throws on Missing

Unlike getContext, the getter from createContext throws when context is missing:

<script>
	import { getUser } from '$lib/context/user'

	// If no ancestor called setUser(), this throws immediately:
	// "Context was not provided"
	const user = getUser()
</script>

This is a feature, not a bug—it catches configuration errors during development rather than causing mysterious undefined errors later.

createContext with Default Values

You can provide a default value that’s used when no provider sets context:

// lib/context/config.ts
import { createContext } from 'svelte'

interface Config {
	apiUrl: string
	timeout: number
	debug: boolean
}

export const [getConfig, setConfig] = createContext<Config>({
	apiUrl: '/api',
	timeout: 5000,
	debug: false
})

Now getConfig() returns the default if no ancestor provided config, instead of throwing.


Consuming Multiple Contexts

Real-world components often need data from several contexts—theme settings, user information, feature flags, toast notifications. Let’s explore patterns for managing this cleanly.

Direct Approach: Multiple getContext Calls

The straightforward approach works well for most components:

<script>
	import { getContext } from 'svelte'

	const theme = getContext('theme')
	const user = getContext('user')
	const locale = getContext('locale')
	const toast = getContext('toast')
</script>

This is explicit, easy to understand, and works perfectly for components that need a handful of contexts. Don’t over-engineer it.

Consolidated Hook for Common Context Sets

If many components need the same combination of contexts, extract a hook:

// src/lib/hooks/useAppContext.ts
import { getContext, hasContext } from 'svelte'

export function useAppContext() {
	return {
		user: getContext('user'),
		theme: hasContext('theme') ? getContext('theme') : { mode: 'light' },
		locale: getContext('locale'),
		toast: getContext('toast')
	}
}
<script>
	import { useAppContext } from '$lib/hooks/useAppContext'

	const { user, theme, locale, toast } = useAppContext()
</script>

Remember: This function must be called during component initialization—you can’t call it inside an event handler.

Rather than one monolithic hook, create focused hooks for each domain:

// src/lib/hooks/useAuth.ts
import { requireContext } from '$lib/context/utils'

export function useAuth() {
	return requireContext('auth', 'Authentication')
}
// src/lib/hooks/useTheme.ts
import { getContextOr } from '$lib/context/utils'

const defaultTheme = { mode: 'light', accent: '#007bff' }

export function useTheme() {
	return getContextOr('theme', defaultTheme)
}
// src/lib/hooks/usePermissions.ts
import { getContextOr } from '$lib/context/utils'

export function usePermissions() {
	const permissions = getContextOr('permissions', [])

	return {
		list: permissions,
		has: (perm: string) => permissions.includes(perm),
		hasAll: (...perms: string[]) => perms.every((p) => permissions.includes(p)),
		hasAny: (...perms: string[]) => perms.some((p) => permissions.includes(p))
	}
}

Usage is clean and self-documenting:

<script>
	import { useAuth } from '$lib/hooks/useAuth'
	import { useTheme } from '$lib/hooks/useTheme'
	import { usePermissions } from '$lib/hooks/usePermissions'

	const auth = useAuth()
	const theme = useTheme()
	const { has: hasPermission } = usePermissions()
</script>

{#if hasPermission('admin')}
	<AdminPanel />
{/if}

This pattern scales well because each hook:

  • Encapsulates its own error handling
  • Provides sensible defaults
  • Can add computed properties (like hasPermission)
  • Is independently testable

The Destructuring Trap: A Common Reactivity Pitfall

There’s a subtle but important gotcha when working with reactive context that trips up many developers. Understanding it will save you hours of debugging.

The Problem: Lost Reactivity

Consider this innocent-looking code:

<script>
	import { getContext } from 'svelte'

	const settings = getContext('settings')
	// settings = { theme: 'light', fontSize: 16, language: 'en' }

	// Destructuring seems convenient...
	const { theme, fontSize, language } = settings
</script>

<div class={theme}>
	<p style="font-size: {fontSize}px">Language: {language}</p>
</div>

Now imagine the provider updates settings.theme to 'dark'. Your component won’t update. It still shows 'light'.

Why This Happens

When you destructure, JavaScript extracts the values at that moment and assigns them to new variables:

const { theme } = settings
// Is equivalent to:
const theme = settings.theme // Copies 'light' into a new variable

The theme variable now holds the string 'light' independently. There’s no ongoing connection to themeSettings.theme. When the provider changes themeSettings.theme to 'dark', your theme variable is unaffected—it’s still 'light'.

Solution 1: Access Properties Directly (Simplest)

The easiest fix is to skip destructuring and access properties on the object:

<script>
	import { getContext } from 'svelte'

	const settings = getContext('settings')
</script>

<!-- Access properties directly—this WILL update -->
<div class={settings.theme}>
	<p style="font-size: {settings.fontSize}px">
		Language: {settings.language}
	</p>
</div>

Each time Svelte renders, it reads the current value of settings.theme. If the provider changed it, you see the new value.

Solution 2: Reactive Destructuring with $derived

If you want the convenience of short variable names, use $derived to create reactive bindings:

<script>
	import { getContext } from 'svelte'

	const settings = getContext('settings')

	// These re-evaluate whenever settings changes
	let theme = $derived(settings.theme)
	let fontSize = $derived(settings.fontSize)
	let language = $derived(settings.language)
</script>

<!-- Now these update automatically -->
<div class={theme}>
	<p style="font-size: {fontSize}px">Language: {language}</p>
</div>

Each $derived creates a reactive binding that re-computes whenever its dependencies change.

Solution 3: Template-Level Destructuring with {@const}

For one-off use in a specific template block, you can destructure right where you need it:

<script>
	import { getContext } from 'svelte'

	const settings = getContext('settings')
</script>

{@const { theme, fontSize, language } = settings}
<div class={theme}>
	<p style="font-size: {fontSize}px">Language: {language}</p>
</div>

The {@const} declaration runs on every render, so it always captures current values. This is useful when you need destructuring within a loop or conditional block.

When Does This Matter?

This trap only affects you when:

  1. The context value is reactive (contains $state internally)
  2. You destructure primitive values (strings, numbers, booleans)
  3. The provider actually changes those values

If your context is read-only configuration that never changes, destructuring is perfectly fine. But when in doubt, access properties directly—it’s always safe.

We’ll explore reactivity patterns in much greater depth in the Making Context Reactive article.


Putting It All Together: A Real-World Example

Let’s apply everything we’ve learned to build a Card component that demonstrates robust context consumption patterns.

The Requirements

Our Card component should:

  • Adapt to theme context when available
  • Work standalone with sensible defaults (portability)
  • Support variants that derive colors from the theme
  • Be fully typed for great developer experience

The Implementation

<!-- src/lib/components/Card.svelte -->
<script lang="ts">
	import { getContext, hasContext } from 'svelte'

	// --- Props ---
	interface Props {
		title?: string
		subtitle?: string
		variant?: 'default' | 'primary' | 'muted'
		children: import('svelte').Snippet
	}

	let { title, subtitle, variant = 'default', children }: Props = $props()

	// --- Theme Context (optional with fallback) ---
	interface Theme {
		mode: 'light' | 'dark'
		colors: {
			primary: string
			background: string
			text: string
			muted: string
			border: string
		}
		borderRadius: string
		shadow: string
	}

	const defaultTheme: Theme = {
		mode: 'light',
		colors: {
			primary: '#007bff',
			background: '#ffffff',
			text: '#1a1a1a',
			muted: '#f5f5f5',
			border: '#e0e0e0'
		},
		borderRadius: '8px',
		shadow: '0 2px 8px rgba(0, 0, 0, 0.1)'
	}

	// Strategy 2: Graceful fallback for optional context
	const theme: Theme = hasContext('theme') ? getContext('theme') : defaultTheme

	// --- Derived Styling ---
	// Use $derived to react to theme changes
	let backgroundColor = $derived(
		{
			default: theme.colors.background,
			primary: theme.colors.primary,
			muted: theme.colors.muted
		}[variant]
	)

	let textColor = $derived(variant === 'primary' ? '#ffffff' : theme.colors.text)
</script>

<article
	class="card"
	class:dark={theme.mode === 'dark'}
	style="
		--card-bg: {backgroundColor};
		--card-text: {textColor};
		--card-border: {theme.colors.border};
		--card-radius: {theme.borderRadius};
		--card-shadow: {theme.shadow};
	"
>
	{#if title}
		<header class="card-header">
			<h3 class="card-title">{title}</h3>
			{#if subtitle}
				<p class="card-subtitle">{subtitle}</p>
			{/if}
		</header>
	{/if}

	<div class="card-body">
		{@render children()}
	</div>
</article>

<style>
	.card {
		background: var(--card-bg);
		color: var(--card-text);
		border: 1px solid var(--card-border);
		border-radius: var(--card-radius);
		box-shadow: var(--card-shadow);
		overflow: hidden;
	}

	.card.dark {
		border-color: rgba(255, 255, 255, 0.1);
	}

	.card-header {
		padding: 1rem 1.5rem;
		border-bottom: 1px solid var(--card-border);
	}

	.card-title {
		margin: 0;
		font-size: 1.25rem;
		font-weight: 600;
	}

	.card-subtitle {
		margin: 0.25rem 0 0;
		opacity: 0.7;
		font-size: 0.875rem;
	}

	.card-body {
		padding: 1.5rem;
	}
</style>

What Makes This Robust

  1. Works anywhere: The default theme ensures the card renders beautifully even without a theme provider

  2. Adapts when context exists: When used within a themed layout, it automatically picks up the app’s colors and styling

  3. Reactive to changes: Using $derived for variant colors means the card updates if the theme toggles between light/dark mode

  4. Fully typed: TypeScript catches mismatches between expected and provided theme structures

  5. CSS custom properties: The bridge between script and styles keeps the component maintainable

Usage Examples

<!-- Works standalone -->
<div class="grid">
	<!-- Works standalone -->
	<Card title="Hello World">
		<p>This card uses default styling.</p>
	</Card>

	<!-- Adapts to theme context -->
	<Card title="Themed Card" variant="primary">
		<p>This card uses your app's primary color.</p>
	</Card>

	<!-- Inside a dark-themed section -->
	<Card title="Dark Mode" subtitle="Automatically adapts">
		<p>Border and shadow adjust for dark backgrounds.</p>
	</Card>
</div>

<style>
	.grid {
		display: grid;
		gap: 1.5rem;
		grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
		margin-top: 2rem;
	}
</style>

Conclusion

Consuming context is the counterpart to providing it, and understanding both sides completes your mental model of how data flows through Svelte’s component tree. While providing context is about broadcasting availability, consuming context is about accessing that availability reliably and safely.

The patterns in this article—from basic getContext calls to sophisticated typed utilities—all address the same fundamental challenge: how do you access data that might or might not exist, in a way that’s type-safe, maintainable, and debuggable? The answer depends on your use case: fail fast for required context, graceful fallbacks for optional context, and explicit null checks when you need to distinguish between the two.

The destructuring trap deserves special emphasis because it catches so many developers off guard. The mental model to remember is simple: primitives are copied, references are shared. When you destructure a primitive from a reactive context object, you get a snapshot that never updates. When you access it through the object (context.theme instead of just theme), you get the current value every time.

With both providing and consuming context mastered, you’re equipped to build applications where data flows cleanly through your component tree—no prop drilling, no coupling through intermediates, just clear declarations of what’s available and what’s needed.


Key Takeaways

Consuming context effectively comes down to understanding a few key principles:

The Core Functions

  • getContext(key) retrieves context from the nearest ancestor. Returns undefined if not found—not an error.
  • hasContext(key) checks existence without retrieving. Use this for optional context patterns.
  • getAllContexts() returns everything as a Map. Great for debugging and context forwarding.

The Timing Rule

All context functions must run during component initialization. Capture what you need at the top of your script, then use those values anywhere.

Handling Missing Context

SituationStrategy
Component can’t work without itFail fast with descriptive error
Nice-to-have enhancementProvide sensible default
Need to distinguish “missing” from “present”Return explicit null
Quick one-off accessOptional chaining (context?.value)

The Destructuring Trap

Destructuring breaks reactivity for primitive values. Either access properties directly on the context object, or use $derived to create reactive bindings.

Type Safety

Use createContext (Svelte 5.40+) for automatic typing and error-on-missing behavior, or create typed utility functions to standardize context access across your app.

With these patterns mastered, you’ll consume context safely and effectively throughout your Svelte applications.


What’s Next

Explore how context behaves in complex component hierarchies in Context Scope and Flow, where you’ll master the “closest wins” rule and context override patterns.


See Also

Official Documentation