A guide to common errors and how to fix them when using Tilia.
Orphan Computation Error
If you’re seeing this error, it means you tried to use a computation (computed, source, or store) that was created outside of a reactive object.
The Error Messages
You may encounter one of these errors:
Cannot access value of an orphan computation
Cannot modify an orphan computation
What Are Orphan Computations?
An “orphan computation” is a computation definition that exists in the “void”—it was created outside of any reactive object (tilia, carve, or derived). It is a computation definition that looks like the returned type but is actually an object containing that definition (shadow type pattern).
Think of it this way:
Computation definition = A description of how to compute a value
Reactive object = The container that brings that definition to life
Without a reactive object, the computation is just a description floating in memory—it can’t actually compute anything.
The Problem Pattern
const[count, setCount]=signal(0)// ❌ WRONG: Creating an orphan computation// This creates a computation definition in the voidconst trouble =computed(()=> count.value *2)// ❌ ERROR: Trying to use the orphan computation// This will throw: "Cannot access value of an orphan computation"const result = trouble *2
let(count, setCount)=signal(0)// ❌ WRONG: Creating an orphan computation// This creates a computation definition in the voidlet trouble =computed(()=> count.value*2)// ❌ ERROR: Trying to use the orphan computation// This will throw: "Cannot access value of an orphan computation"let result = trouble *2
In this example:
trouble is assigned a computation definition (not a value)
When you try to use trouble in trouble * 2, JavaScript tries to access its value
The proxy intercepts this and throws an error because the computation was never attached to a reactive graph
const[count, setCount]=signal(0)// ✅ CORRECT: Define computed directly in a reactive objectconst p =tilia({
double:computed(()=> count.value *2)})// ✅ WORKS: Access the computed value through the reactive objectconsole.log(p.double)// Returns the computed valueconst result = p.double *2// Works perfectly
let(count, setCount)=signal(0)// ✅ CORRECT: Define computed directly in a reactive objectlet p =tilia({
double:computed(()=> count.value*2)})// ✅ WORKS: Access the computed value through the reactive objectJs.log(p.double)// Returns the computed valuelet result = p.double*2// Works perfectly
Why Does This Restriction Exist?
This safety feature was introduced in Tilia v4.0 to prevent a common class of runtime errors called “zombie computations” or “orphan computations.”
Without This Protection (Pre-v4.0)
In earlier versions, orphan computations would:
Silently fail or return undefined
Cause obscure JavaScript errors deep in the stack
Make it very difficult to debug what went wrong
With This Protection (v4.0+)
Now you get:
Immediate, clear error at the point of misuse
Descriptive message explaining the problem
Link to this documentation with examples
The error message guides you to write correct code from the start, eliminating a whole class of hard-to-debug issues.
The Three Reactive Contexts
Computations must be created inside one of these three contexts:
const double =derived(()=> count.value *2)console.log(double.value)// Access via .value
let double =derived(()=> count.value*2)Js.log(double.value)// Access via .value
Common Scenarios
Scenario 1: Helper Functions
❌ Wrong:
functionmakeFullName(firstName, lastName){// Orphan created in the void!// Also: business logic polluted with reactive framework concepts (signals, computed)returncomputed(()=>`${firstName.value}${lastName.value}`)}const name =makeFullName(first, last)console.log(name)// ERROR: Cannot access value of an orphan computation
let makeFullName =(firstName, lastName)=>{// Orphan created in the void!// Also: business logic polluted with reactive framework concepts (signals, computed)computed(()=>`${firstName.value}${lastName.value}`)}let name =makeFullName(first, last)Js.log(name)// ERROR: Cannot access value of an orphan computation
❌ Still Wrong (even if used in tilia):
functionmakeFullName(firstName, lastName){// Creates orphan! Also mixes business logic with framework concernsreturncomputed(()=>`${firstName.value}${lastName.value}`)}const user =tilia({
fullName:makeFullName(first, last)// Works, but wrong architecture})
let makeFullName =(firstName, lastName)=>{// Creates orphan! Also mixes business logic with framework concernscomputed(()=>`${firstName.value}${lastName.value}`)}let user =tilia({
fullName:makeFullName(first, last)// Works, but wrong architecture})
✅ Correct - Separate business logic from reactive wiring:
// Pure function: business logic (testable, reusable)functionformatFullName(firstName, lastName){return`${firstName}${lastName}`}// Reactive wiring: done inside tiliaconst user =tilia({
firstName:signal("Alice"),
lastName:signal("Smith"),
fullName:computed(()=>formatFullName(user.firstName, user.lastName))})console.log(user.fullName)// Works! "Alice Smith"
// Pure function: business logic (testable, reusable)let formatFullName =(firstName, lastName)=>{`${firstName}${lastName}`}// Reactive wiring: done inside tilialet user =tilia({
firstName:signal("Alice"),
lastName:signal("Smith"),
fullName:computed(()=>formatFullName(user.firstName, user.lastName))})Js.log(user.fullName)// Works! "Alice Smith"
Why? This pattern:
✅ Never creates orphan computations
✅ Keeps business logic pure and framework-agnostic (no signals, no computed, no library pollution)
✅ Separates business logic (testable formatFullName()) from reactive wiring
✅ Makes it clear where reactive boundaries are
✅ The formatFullName() function can be reused in non-reactive contexts (CLI tools, tests, other frameworks)
// Create a standalone reactive value with derived()const shared =derived(()=>expensiveCalculation())// Reference it from other objectsconst obj1 =tilia({
result:computed(()=> shared.value *2)})const obj2 =tilia({
result:computed(()=> shared.value +10)})
// Create a standalone reactive value with derived()let shared =derived(()=>expensiveCalculation())// Reference it from other objectslet obj1 =tilia({
result:computed(()=> shared.value*2)})let obj2 =tilia({
result:computed(()=> shared.value+10)})
let obj =tilia({
value:computed(()=>if condition { a.value}else{ b.value})})
✅ Correct (standalone with derived):
const value =derived(()=> condition ? a.value : b.value)// Use it anywhereconsole.log(value.value)
let value =derived(()=>if condition { a.value}else{ b.value})// Use it anywhereJs.log(value.value)
The “Glue Zone” Should Not Exist
The “Glue Zone” is the space between creating a computation definition and inserting it into a reactive object. This zone should not exist at all.
❌ Glue Zone Exists (Dangerous)
// Computation definition createdconst trouble =computed(()=>...)// ← GLUE ZONE: computation floats in the void// ... potentially many lines of code ...// ... risk of using it as a value or passing it to wrong place ...const app =tilia({
value: trouble // ← Finally inserted})
// Computation definition createdlet trouble =computed(()=>...)// ← GLUE ZONE: computation floats in the void// ... potentially many lines of code ...// ... risk of using it as a value or passing it to wrong place ...let app =tilia({
value: trouble // ← Finally inserted})
Problem: The computation exists as an orphan between creation and insertion. It can be accidentally used as a value or passed to the wrong place.
✅ No Glue Zone (Correct)
const app =tilia({
value:computed(()=>...)// ← Created inline, no glue zone})
let app =tilia({
value:computed(()=>...)// ← Created inline, no glue zone})
Solution: The computation is created directly inside the reactive object. No opportunity for misuse.
Preferred Pattern: Keep related computations together in a single reactive object using tilia() or carve(). This makes your business logic easier to reason about compared to fragmenting it into many separate signal() or derived() values.
The safety proxy catches cases where a glue zone exists, ensuring computation definitions don’t escape into places where they’d be treated as values.
Technical Details
Under the hood, when you create a computed(), source(), or store() outside a reactive object:
It returns a SafeProxy wrapping the computation definition.
The proxy allows Tilia’s internal properties to be accessed.
Any other property access throws the error.
This proxy is transparent when used correctly (inside tilia/carve/derived), but protects you from misuse.
Orphan Computation Error
If you’re seeing this error, it means you tried to use a computation (
computed,source, orstore) that was created outside of a reactive object.The Error Messages
You may encounter one of these errors:
What Are Orphan Computations?
An “orphan computation” is a computation definition that exists in the “void”—it was created outside of any reactive object (
tilia,carve, orderived). It is a computation definition that looks like the returned type but is actually an object containing that definition (shadow type pattern).Think of it this way:
Without a reactive object, the computation is just a description floating in memory—it can’t actually compute anything.
The Problem Pattern
In this example:
troubleis assigned a computation definition (not a value)troubleintrouble * 2, JavaScript tries to access its valueThe Correct Pattern
Define computations directly inside reactive objects:
Why Does This Restriction Exist?
This safety feature was introduced in Tilia v4.0 to prevent a common class of runtime errors called “zombie computations” or “orphan computations.”
Without This Protection (Pre-v4.0)
In earlier versions, orphan computations would:
undefinedWith This Protection (v4.0+)
Now you get:
The error message guides you to write correct code from the start, eliminating a whole class of hard-to-debug issues.
The Three Reactive Contexts
Computations must be created inside one of these three contexts:
1.
tilia({ ... })The primary way to create reactive objects:
2.
carve({ derived } => { ... })For creating objects that reference themselves during construction:
3.
derived(() => value)For creating standalone reactive values:
Common Scenarios
Scenario 1: Helper Functions
❌ Wrong:
❌ Still Wrong (even if used in tilia):
✅ Correct - Separate business logic from reactive wiring:
Why? This pattern:
formatFullName()) from reactive wiringformatFullName()function can be reused in non-reactive contexts (CLI tools, tests, other frameworks)Scenario 2: Reusable Computations
❌ Wrong:
✅ Correct:
Scenario 3: Conditional Computations
❌ Wrong:
✅ Correct (inside tilia):
✅ Correct (standalone with derived):
The “Glue Zone” Should Not Exist
The “Glue Zone” is the space between creating a computation definition and inserting it into a reactive object. This zone should not exist at all.
❌ Glue Zone Exists (Dangerous)
Problem: The computation exists as an orphan between creation and insertion. It can be accidentally used as a value or passed to the wrong place.
✅ No Glue Zone (Correct)
Solution: The computation is created directly inside the reactive object. No opportunity for misuse.
Preferred Pattern: Keep related computations together in a single reactive object using
tilia()orcarve(). This makes your business logic easier to reason about compared to fragmenting it into many separatesignal()orderived()values.The safety proxy catches cases where a glue zone exists, ensuring computation definitions don’t escape into places where they’d be treated as values.
Technical Details
Under the hood, when you create a
computed(),source(), orstore()outside a reactive object:This proxy is transparent when used correctly (inside
tilia/carve/derived), but protects you from misuse.Related Documentation
Still Having Issues?
If you’re still encountering this error and believe it’s a false positive, please:
The Tilia community is here to help!