When Simple APIs Trip You Up
Even experienced Svelte developers occasionally stumble with context. The API is simple on the surface—just setContext and getContext—but subtle behaviors can lead to confusing bugs. Values that don’t update. Context that mysteriously returns undefined. Components that throw errors about initialization timing.
This article catalogs the most common context pitfalls, explains why they occur, and provides clear solutions. We’ll also cover debugging techniques to quickly diagnose context issues when they arise. Consider this your troubleshooting guide for everything context-related.
By understanding these pitfalls, you’ll write more robust context code and spend less time debugging mysterious failures.
Pitfall 1: Calling Context Functions Outside Initialization
The most common error: attempting to call setContext or getContext outside of component initialization.
The Error
Error: `setContext(...)` can only be called during component initialization
Error: `getContext(...)` can only be called during component initialization What Causes It
Context functions must be called during the synchronous execution of your <script> block when the component first mounts. They cannot be called in:
- Event handlers
$effectcallbackssetTimeoutorsetIntervalcallbacks- Promise
.then()callbacks - After an
awaitstatement - Any asynchronous code
Examples of the Mistake
<script>
import { setContext, getContext } from 'svelte'
// ❌ Inside event handler
function handleClick() {
setContext('late', 'value') // Error!
}
// ❌ Inside effect
$effect(() => {
const value = getContext('something') // Error!
})
// ❌ Inside setTimeout
setTimeout(() => {
setContext('delayed', 'value') // Error!
}, 0)
// ❌ After await
async function setup() {
const data = await fetch('/api/data')
setContext('data', data) // Error!
}
setup()
// ❌ In Promise callback
fetch('/api/config')
.then((response) => response.json())
.then((config) => {
setContext('config', config) // Error!
})
</script> The Solution
Always capture or set context at the top level of your script, then use the captured values elsewhere:
<script>
import { setContext, getContext } from 'svelte'
// ✅ Set context during initialization with reactive state
let config = $state(null)
let loading = $state(true)
setContext('config', {
get data() {
return config
},
get loading() {
return loading
}
})
// ✅ Capture context during initialization
const theme = getContext('theme')
const auth = getContext('auth')
// Now use freely in event handlers
function handleClick() {
console.log('Theme:', theme.mode) // ✅ Works!
console.log('User:', auth.user?.name) // ✅ Works!
}
// And in effects
$effect(() => {
document.body.classList.toggle('dark', theme.isDark) // ✅ Works!
})
// And with async operations
async function loadConfig() {
loading = true
const response = await fetch('/api/config')
config = await response.json() // Updates the context value!
loading = false
}
loadConfig()
</script> Why This Restriction Exists
Context is part of the component tree’s structure. When a child component initializes, it looks up the tree for context that ancestors have set. If context could be set later (in event handlers, etc.), children would have already tried to read it and failed.
Think of it like building a house: you need to install the plumbing (context) while building the walls (component initialization), not after the house is finished (runtime).
Pitfall 2: Reassigning State Objects
One of the trickiest pitfalls: breaking the connection between provider and consumers by reassigning state.
The Symptom
Context updates work initially, then suddenly stop. Some consumers see updates, others don’t. Or updates stop after a specific action.
What Causes It
When you provide a reactive object via context, both provider and consumers hold references to the same object. If you reassign the variable in the provider to a new object, consumers still reference the old object.
The Mistake
<!-- Provider.svelte -->
<script>
import { setContext } from 'svelte'
let user = $state({ name: 'Alice', score: 0 })
setContext('user', user)
function updateScore() {
user.score++ // ✅ This works - mutating the object
}
function resetUser() {
user = { name: 'Guest', score: 0 } // ❌ This breaks consumers!
// Provider now points to NEW object
// Consumers still point to OLD object
}
</script> <!-- Consumer.svelte -->
<script>
import { getContext } from 'svelte'
const user = getContext('user')
// user references the ORIGINAL object
// After resetUser(), this is orphaned
</script>
<p>{user.name}: {user.score}</p>
<!-- After resetUser(), still shows old values --> Visual Explanation
Initial state:
Provider.user ───────┐
├──> { name: 'Alice', score: 0 }
Consumer.user ───────┘
After user.score++ (mutation):
Provider.user ───────┐
├──> { name: 'Alice', score: 1 } ✅ Both see change
Consumer.user ───────┘
After user = { name: 'Guest', score: 0 } (reassignment):
Provider.user ───────────> { name: 'Guest', score: 0 } ← NEW object
Consumer.user ───────────> { name: 'Alice', score: 1 } ← OLD object (orphaned!)
❌ Consumer never sees new object The Solution
Always mutate properties, never reassign the entire object:
<script>
import { setContext } from 'svelte'
let user = $state({ name: 'Alice', score: 0 })
setContext('user', user)
function updateScore() {
user.score++ // ✅ Mutation
}
function resetUser() {
// ✅ Mutate properties individually
user.name = 'Guest'
user.score = 0
}
// ✅ Or use Object.assign for multiple properties
function updateUser(updates) {
Object.assign(user, updates)
}
</script> Alternative: Wrap in Container Object
If you need to replace the entire value, wrap it in a container:
<script>
import { setContext } from 'svelte'
let container = $state({ user: { name: 'Alice', score: 0 } })
setContext('userContainer', container)
function resetUser() {
// ✅ Mutating container.user property
container.user = { name: 'Guest', score: 0 }
}
</script> Consumer accesses via the container:
<script>
const container = getContext('userContainer')
</script>
<!-- Access through container --><p>{container.user.name}: {container.user.score}</p> Pitfall 3: Destructuring Reactive Values
Destructuring captures values at a point in time, breaking reactivity.
The Symptom
Variables from destructured context never update, even though the source changes.
The Mistake
<script>
import { getContext } from 'svelte'
const settings = getContext('settings')
// ❌ Destructuring captures current VALUES
const { theme, fontSize, language } = settings
// theme = 'light' (a string, frozen in time)
// fontSize = 16 (a number, frozen in time)
</script>
<!-- These never update! -->
<div class={theme}>
<!-- Always 'light' -->
<p style="font-size: {fontSize}px">
<!-- Always 16 -->
{language}
<!-- Always the initial value -->
</p>
</div> Why This Happens
Destructuring is syntactic sugar for assignment:
const { theme } = settings
// Is equivalent to:
const theme = settings.theme // Copies the primitive value For primitives (strings, numbers, booleans), you get a copy. For objects, you get a reference—but still captured at that moment.
Solutions
Solution 1: Don’t destructure, access directly
<script>
const settings = getContext('settings')
// Keep the object reference
</script>
<!-- Access properties directly - always current -->
<div class={settings.theme}>
<p style="font-size: {settings.fontSize}px">
{settings.language}
</p>
</div> Solution 2: Reactive “destructuring” with $derived
<script>
const settings = getContext('settings')
// ✅ Each $derived re-evaluates when settings changes
let theme = $derived(settings.theme)
let fontSize = $derived(settings.fontSize)
let language = $derived(settings.language)
</script>
<!-- Now these update! -->
<div class={theme}>
<p style="font-size: {fontSize}px">
{language}
</p>
</div> Solution 3: Destructure methods only
Methods (functions) can be safely destructured—they’re references, not values:
<script>
const cart = getContext('cart')
// ✅ Safe - these are function references
const { addItem, removeItem, clear } = cart
function handleAdd(product) {
addItem(product) // Works correctly
}
</script> Pitfall 4: Missing Provider
Consuming context that was never provided.
The Symptom
getContext returns undefined, leading to “Cannot read property X of undefined” errors when you try to use the context.
The Mistake
<!-- App.svelte - forgot the provider! -->
<script>
import Dashboard from './Dashboard.svelte'
</script>
<!-- No AuthProvider wrapping Dashboard -->
<Dashboard /> <!-- Dashboard.svelte -->
<script>
import { getContext } from 'svelte'
const auth = getContext('auth') // Returns undefined!
// Later...
console.log(auth.user.name) // Error: Cannot read property 'user' of undefined
</script> Solutions
Solution 1: Check with hasContext
<script>
import { getContext, hasContext } from 'svelte'
if (!hasContext('auth')) {
throw new Error('Auth context not found. ' + 'Wrap this component in an AuthProvider.')
}
const auth = getContext('auth')
</script> Solution 2: Provide defaults for optional context
<script>
import { getContext, hasContext } from 'svelte'
const defaultAuth = {
user: null,
isAuthenticated: false,
login: () => console.warn('No auth provider'),
logout: () => console.warn('No auth provider')
}
const auth = hasContext('auth') ? getContext('auth') : defaultAuth
</script> Solution 3: Use createContext which throws
With Svelte 5.40+‘s createContext, the getter throws automatically:
// auth.svelte.ts
import { createContext } from 'svelte'
export const [getAuth, setAuth] = createContext<AuthContext>()
// Consumer.svelte
const auth = getAuth() // Throws "Context was not provided" if missing Solution 4: Create a safe getter utility
// lib/context/utils.ts
import { getContext, hasContext } from 'svelte'
export function requireContext<T>(key: any, name: string): T {
if (!hasContext(key)) {
throw new Error(
`${name} context is required but was not found. ` +
`Ensure this component is rendered inside the appropriate provider.`
)
}
return getContext(key)
}
// Usage
const auth = requireContext('auth', 'Auth') Pitfall 5: Provider in Wrong Position
Provider is present but not an ancestor of the consumer.
The Symptom
Context returns undefined even though the provider exists in the app.
The Mistake
<!-- App.svelte -->
<script>
import AuthProvider from './AuthProvider.svelte'
import Dashboard from './Dashboard.svelte'
</script>
<!-- Provider and Dashboard are SIBLINGS, not parent-child -->
<AuthProvider />
<Dashboard />
<!-- Can't access auth context! --> Context only flows downward from ancestors to descendants. Siblings can’t see each other’s context.
The Solution
Ensure the provider wraps the consumer:
<!-- App.svelte -->
<script>
import AuthProvider from './AuthProvider.svelte'
import Dashboard from './Dashboard.svelte'
</script>
<!-- Provider WRAPS Dashboard -->
<AuthProvider>
<Dashboard />
<!-- Now has access! -->
</AuthProvider> Visual Debugging
❌ Wrong - siblings:
App
├── AuthProvider (sets 'auth')
└── Dashboard (getContext('auth') → undefined)
✅ Correct - ancestor/descendant:
App
└── AuthProvider (sets 'auth')
└── Dashboard (getContext('auth') → ✓) Pitfall 6: Wrong Key
Using a different key for get than what was used for set.
The Symptom
Context returns undefined even with correct provider/consumer relationship.
The Mistake
<!-- Provider.svelte -->
<script>
import { setContext } from 'svelte'
setContext('userAuth', authData) // Key: 'userAuth'
</script>
<!-- Consumer.svelte -->
<script>
import { getContext } from 'svelte'
const auth = getContext('auth') // Key: 'auth' ← Doesn't match!
// Returns undefined
</script> This is especially easy with:
- Typos:
'user'vs'users' - Case sensitivity:
'Auth'vs'auth' - Different conventions:
'user-auth'vs'userAuth'
Solutions
Solution 1: Export the key
// context/keys.ts
export const AUTH_KEY = 'auth'
export const CART_KEY = 'cart'
export const THEME_KEY = 'theme' <!-- Both files import the same key -->
import {AUTH_KEY} from './context/keys' setContext(AUTH_KEY, value) // Provider getContext(AUTH_KEY)
// Consumer Solution 2: Use Symbols
Symbols are unique, so key collisions are impossible:
// context/auth.svelte.ts
const AUTH_KEY = Symbol('auth')
export function createAuthContext() {
return setContext(AUTH_KEY /* ... */)
}
export function getAuthContext() {
return getContext(AUTH_KEY)
} Solution 3: Use createContext
createContext manages keys internally:
// No key to get wrong!
export const [getAuth, setAuth] = createContext<AuthContext>() Pitfall 7: Context in Dynamic Components
Components mounted outside the normal tree lose context.
The Symptom
Dynamically mounted components (modals, tooltips, portals) can’t access context.
The Mistake
<script>
import { mount } from 'svelte'
import Modal from './Modal.svelte'
function openModal() {
// Modal mounted at document.body, outside Svelte tree
mount(Modal, { target: document.body })
// Modal has NO access to any context!
}
</script> The Solution
Forward context explicitly using getAllContexts:
<script>
import { mount, getAllContexts } from 'svelte'
import Modal from './Modal.svelte'
// Capture context during initialization
const appContext = getAllContexts()
function openModal() {
mount(Modal, {
target: document.body,
context: appContext // Forward all context
})
}
</script> Or create a portal component that handles this:
<!-- Portal.svelte -->
<script>
import { mount, unmount, getAllContexts } from 'svelte'
let { target = document.body, children } = $props()
const context = getAllContexts()
$effect(() => {
// Create a wrapper component that re-provides context
const wrapper = mount(ContextForwarder, {
target,
context,
props: { content: children }
})
return () => unmount(wrapper)
})
</script> Pitfall 8: Expecting Reactivity Without $state
Plain objects in context aren’t reactive.
The Symptom
Context values don’t update when the source changes.
The Mistake
<script>
import { setContext } from 'svelte'
// Plain object, not reactive
const user = { name: 'Alice', score: 0 }
setContext('user', user)
function updateScore() {
user.score++ // Value changes in memory...
// But Svelte doesn't know! No re-renders triggered.
}
</script> The Solution
Use $state for reactive context values:
<script>
import { setContext } from 'svelte'
// Reactive with $state
let user = $state({ name: 'Alice', score: 0 })
setContext('user', user)
function updateScore() {
user.score++ // ✅ Svelte tracks this, triggers re-renders
}
</script> Pitfall 9: Using Context for SSR-Sensitive Data
Context mutations during SSR can leak between requests.
The Symptom
Users see other users’ data. Data persists incorrectly across requests.
The Mistake
// lib/stores/global.svelte.ts
// Global mutable state - DANGEROUS with SSR!
export const globalUser = $state({ name: null, id: null }) <!-- +page.svelte -->
<script>
import { globalUser } from '$lib/stores/global.svelte'
let { data } = $props()
// Mutating global state during render
globalUser.name = data.user.name // ❌ May leak to other requests!
</script> On the server, a single Node.js process handles multiple requests. Global state persists between requests.
The Solution
Use context for request-scoped data:
<!-- +layout.svelte -->
<script>
import { setContext } from 'svelte'
let { data } = $props()
// Create fresh context for each request
// Context is automatically isolated per-component-tree
setContext('user', data.user)
</script>
<slot /> Context is tied to a specific component tree. Each request gets its own component tree, so context is naturally isolated.
Pitfall 10: Context in Tight Loops
Accessing context repeatedly in loops or frequently-called functions.
The Symptom
Sluggish UI performance when many components consume the same context, or when context-dependent functions are called frequently.
The Mistake
<script>
import { getContext } from 'svelte'
let { items } = $props()
// ❌ Getting context inside a function called per item
function formatItem(item) {
const settings = getContext('settings') // Called many times!
return `${item.name} (${settings.currency})`
}
</script>
{#each items as item}
<p>{formatItem(item)}</p>
{/each} While getContext is fast, calling it repeatedly in loops adds unnecessary overhead. More importantly, this pattern suggests a misunderstanding of how context works—you only need to get it once.
The Solution
Get context once at the top level of your script:
<script>
import { getContext } from 'svelte'
let { items } = $props()
// ✅ Get context once during initialization
const settings = getContext('settings')
function formatItem(item) {
return `${item.name} (${settings.currency})`
}
</script>
{#each items as item (item.id)}
<p>{formatItem(item)}</p>
{/each} The context value doesn’t change during the component’s lifecycle (though its properties might if it’s reactive). Capture the reference once and reuse it throughout your component.
Related Pattern: Avoiding Context in Derived Values
Don’t access context inside $derived calculations that run frequently:
<script>
import { getContext } from 'svelte'
let { price } = $props()
// ❌ Context access inside derived (works but unclear intent)
let formatted = $derived(() => {
const settings = getContext('settings')
return new Intl.NumberFormat(settings.locale, {
style: 'currency',
currency: settings.currency
}).format(price)
})
</script> <script>
import { getContext } from 'svelte'
let { price } = $props()
// ✅ Get context at top level, use in derived
const settings = getContext('settings')
let formatted = $derived(
new Intl.NumberFormat(settings.locale, {
style: 'currency',
currency: settings.currency
}).format(price)
)
</script> Both work, but the second version makes the data flow clearer and follows the established pattern of capturing context during initialization.
Debugging Techniques
When context isn’t working, these techniques help diagnose the issue.
Technique 1: Context Inspector Component
Create a component that displays all available context:
<!-- ContextDebugger.svelte -->
<script>
import { getAllContexts } from 'svelte'
let { show = true } = $props()
const contexts = getAllContexts()
function formatValue(value) {
if (value === undefined) return 'undefined'
if (value === null) return 'null'
if (typeof value === 'function') return '[Function]'
try {
return JSON.stringify(value, null, 2)
} catch {
return String(value)
}
}
function formatKey(key) {
if (typeof key === 'symbol') {
return key.toString()
}
return String(key)
}
</script>
{#if show && import.meta.env.DEV}
<details class="context-debugger" open>
<summary>🔍 Context ({contexts.size} keys)</summary>
{#if contexts.size === 0}
<p class="empty">No context available at this position</p>
{:else}
<ul>
{#each [...contexts.entries()] as [key, value] (key)}
<li>
<strong class="key">{formatKey(key)}</strong>
<pre class="value">{formatValue(value)}</pre>
</li>
{/each}
</ul>
{/if}
</details>
{/if}
<style>
.context-debugger {
position: fixed;
bottom: 1rem;
right: 1rem;
background: #1e1e2e;
color: #cdd6f4;
padding: 0.75rem;
border-radius: 8px;
font-family: 'Fira Code', monospace;
font-size: 12px;
max-width: 400px;
max-height: 400px;
overflow: auto;
z-index: 9999;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
summary {
cursor: pointer;
font-weight: bold;
padding: 0.25rem 0;
}
ul {
list-style: none;
padding: 0;
margin: 0.5rem 0 0;
}
li {
padding: 0.5rem;
border-bottom: 1px solid #313244;
}
li:last-child {
border-bottom: none;
}
.key {
color: #89b4fa;
}
.value {
margin: 0.25rem 0 0;
padding: 0.5rem;
background: #313244;
border-radius: 4px;
white-space: pre-wrap;
word-break: break-all;
color: #a6e3a1;
}
.empty {
color: #f38ba8;
font-style: italic;
}
</style> Drop this anywhere to see what context is available:
<!-- SomeDeeplyNestedComponent.svelte -->
<script>
import ContextDebugger from '$lib/debug/ContextDebugger.svelte'
</script>
<div>
<!-- Component content -->
</div>
<ContextDebugger /> Technique 2: Console Logging in Providers
Add development logging to trace context setup:
<!-- AuthProvider.svelte -->
<script>
import { setContext } from 'svelte'
let { children } = $props()
let user = $state(null)
const context = {
get user() {
return user
},
get isAuthenticated() {
return user !== null
}
}
setContext('auth', context)
// Development logging
if (import.meta.env.DEV) {
console.log('🔐 AuthProvider: Context set', context)
$effect(() => {
console.log('🔐 AuthProvider: User changed', user)
})
}
</script>
{@render children()} Technique 3: Provider Stack Tracking
Track the provider hierarchy:
<!-- Add to each provider -->
<script>
import { setContext, getContext, hasContext } from 'svelte'
// Track provider stack
const parentStack = hasContext('__debug_providers') ? getContext('__debug_providers') : []
const myStack = [...parentStack, 'AuthProvider']
setContext('__debug_providers', myStack)
if (import.meta.env.DEV) {
console.log('Provider stack:', myStack.join(' → '))
}
</script> Output:
Provider stack: ThemeProvider → AuthProvider
Provider stack: ThemeProvider → AuthProvider → CartProvider Technique 4: Checking Consumer Position
Verify where in the tree a consumer is:
<script>
import { getContext, hasContext, getAllContexts } from 'svelte'
const available = getAllContexts()
const hasAuth = hasContext('auth')
const hasTheme = hasContext('theme')
if (import.meta.env.DEV) {
console.group('Component: UserProfile')
console.log('Available context keys:', [...available.keys()])
console.log('Has auth context:', hasAuth)
console.log('Has theme context:', hasTheme)
console.groupEnd()
}
</script> Technique 5: Error Boundary Wrapping
Catch context errors gracefully:
<!-- SafeContextConsumer.svelte -->
<script>
import { getContext, hasContext } from 'svelte'
let { contextKey, fallback, children } = $props()
let error = null
let context = null
try {
if (!hasContext(contextKey)) {
throw new Error(`Context '${contextKey}' not found`)
}
context = getContext(contextKey)
} catch (e) {
error = e.message
if (import.meta.env.DEV) {
console.error(`Context error: ${e.message}`)
}
}
</script>
{#if error}
{#if fallback}
{@render fallback(error)}
{:else}
<div class="context-error">
Context Error: {error}
</div>
{/if}
{:else}
{@render children(context)}
{/if} Quick Reference: Symptoms and Solutions
| Symptom | Likely Cause | Solution |
|---|---|---|
| “can only be called during initialization” | Context function in async code | Call at top level, capture value |
Context returns undefined | Missing provider | Add provider as ancestor |
Context returns undefined | Wrong key | Use shared keys, Symbols, or createContext |
Context returns undefined | Provider is sibling not ancestor | Restructure to wrap consumer |
| Values don’t update | Plain object, not $state | Use $state for reactive values |
| Values stop updating after certain action | Reassigned state object | Mutate properties, don’t reassign |
| Destructured values don’t update | Captured primitive values | Access via object or use $derived |
| Modal/portal can’t access context | Mounted outside tree | Forward context with getAllContexts |
| SSR data leaking between users | Mutating global state | Use context for request-scoped data |
| Sluggish performance with many consumers | Context in loops/frequent calls | Get context once at top level |
Conclusion
Context pitfalls aren’t random bugs—they’re predictable consequences of how JavaScript handles values, how Svelte manages reactivity, and how component trees establish hierarchical relationships. Once you understand these underlying mechanisms, the pitfalls become intuitive rather than mysterious.
The timing constraint exists because context is structural. The reassignment problem exists because JavaScript passes object references, not the variables themselves. The destructuring trap exists because primitives are copied by value. The provider positioning requirement exists because context flows downward through the tree. Each pitfall has a logical cause rooted in fundamental language and framework behaviors.
The debugging techniques in this article—context inspectors, provider stack tracking, strategic logging—transform context from a black box into a transparent system you can observe and reason about. When something goes wrong, you can systematically identify whether it’s a timing issue, a reference issue, a tree structure issue, or a key mismatch.
Most importantly, these pitfalls become increasingly rare as you internalize the patterns. Call context functions at the top level. Mutate properties, don’t reassign objects. Access through the object, don’t destructure primitives. Wrap consumers in providers. Use shared keys or Symbols. Follow these rules consistently, and context becomes a reliable, predictable tool.
Key Takeaways
Context pitfalls generally fall into a few categories:
Timing issues — Context functions must be called during component initialization. Capture values early, use them later.
Reference issues — Don’t reassign state objects; mutate properties. Don’t destructure primitives; access via object.
Tree structure issues — Providers must be ancestors, not siblings. Dynamic components need context forwarded explicitly.
Key mismatches — Use shared constants, Symbols, or
createContextto avoid typos.Reactivity issues — Use
$statefor reactive values. Plain objects aren’t tracked.SSR issues — Don’t mutate global state. Use context for request-scoped data.
When debugging, use getAllContexts() to see what’s available, add logging to providers, and check that providers are ancestors of consumers. With these patterns recognized, you’ll quickly diagnose and fix context issues when they arise.
What’s Next
Understand when context is the right choice in Context vs Other Patterns, comparing context to stores, props, events, and building a decision framework.
See Also
Official Documentation
- Svelte Context — Official guide
- createContext — Type-safe context
Related Articles
- Context Best Practices — Professional patterns
- Reactive Context Fundamentals — Reactive context
- Providing Context — Provider patterns