Error handling
Quantum calls throw ordinary JavaScript errors. Most come from the handle layer, which checks your call before anything reaches the simulator and names the method you called. The rest come from the WASM simulator itself and carry a type prefix.
Errors from handles
These are the mistakes a game can make with a handle, and the message each one throws:
| Situation | Message (N and M are handle ids) | Fix |
|---|---|---|
| Any call before the module has loaded | QuantumForge not loaded. Call ensureLoaded() first and await it. | await ensureLoaded() at startup |
quantum(1), or a single declared value | quantum(1): a quantum property needs at least 2 values. (a RangeError) | Declare two or more values |
| The same value declared twice | quantum(["a", "a"]): value "a" is declared twice. | Make every value unique |
| More values than the build allows | quantum(): a property with 4 values exceeds the loaded build's maximum of 3 values per property. | Declare fewer values, or load a build that allows more |
| A value the property was not declared with | "maybe" is not a value of quantum property #N; its values are [false, true] (or an index 0..1). (a RangeError) | Pass a declared value or its index |
flip() on a property that does not have two values | flip() needs a property with 2 values; quantum property #N has 3. Use next() or cycle() instead. | next() or cycle() on a qutrit |
y() on a property that does not have two values | Y gate requires dimension 2 (qubit) | y() is a qubit gate |
swap() or iSwap() passed the handle it is called on | swap(): quantum property #N cannot swap with itself. | Pass a different property |
swap() or iSwap() across different numbers of values | iSwap(): quantum property #N has 2 values and #M has 3. Both need the same number of values. | Pair properties with the same number of values |
A when predicate on the gate's own target | flip(): a gate on quantum property #N cannot be conditioned on that same property. A when predicate must read a different property. | Predicate on a different property |
A fraction or angle that is NaN or infinite | flip fraction must be a finite number, got NaN. (a RangeError) | Pass a finite number |
| Any call on a disposed handle | Quantum property #N was disposed and can no longer be used. | Stop using the handle after dispose() |
| A predicate built before its property was disposed | Predicate on quantum property #N cannot be used: the property was disposed. | Build predicates from live handles |
| Forcing an outcome with zero probability | forcedMeasure(): value true of quantum property #N has zero probability in the current state, so it cannot be forced. Force only an outcome a measurement could give. | Force only outcomes a measurement could give |
The y() message comes from the simulator; the rest come from the handle layer, which checks the call before the gate runs. Forced measurement is checked the same way: forcedMeasure() (the method and the free function) and forcedMeasureWhen() throw on an impossible outcome instead of measuring.
A disposed handle is often one that stayed on an entity after EntityManager.remove() disposed it. Move the handle off the entity before removing it if you still need it. See Lifecycle: handles on entities.
Errors from the simulator
Errors thrown by the WASM simulator are Error instances whose message starts with a type prefix:
| Prefix | Meaning | Common cause |
|---|---|---|
[QuantumForgeStateSizeError] | State vector would exceed the maximum size | Too many entangled qudits; the tensor product would be too large |
[QuantumForgeOutOfMemoryError] | WASM memory allocation failed | WASM heap exhausted |
[QuantumForgeError] | General quantum operation error | Cross-simulation entanglement on raw properties |
A helper keeps the matching readable:
function isQuantumError(e: unknown, type: "StateSizeError" | "OutOfMemoryError" | "Error"): boolean {
return e instanceof Error && e.message.startsWith(`[QuantumForge${type}]`);
}Hitting the qudit limit
An interaction that would put more qudits in one shared state than the build allows throws. The handle layer rewrites the simulator's message into one that names your call:
flip(): this would put 13 qudits in one entangled state, and the loaded build holds at most 12. Dispose handles you no longer need to free qudits.The limit is 12 qudits in the Qutrit Edition and 20 in the Qubit Edition. When it throws, both properties stay in their own states and remain usable.
Don't plan on catching it, though. Each caught throw leaks WASM memory, and after enough of them every later call fails. A game that expects to run near the limit should check capacity before the interaction:
import { getMaxQudits } from "quantum-forge/quantum";
if (ball.exists.numActiveQudits() >= getMaxQudits()) {
// state is full: stay classical this time
} else {
ball.exists.iSwap(fresh, 0.5);
}If the limit keeps coming up, check that handles are disposed when their entity dies. A disposed property stops counting against the limit.
Proactive OOM guard
The simulator estimates the size of a tensor product before allocating memory. A tensor product happens the first time two properties in separate states interact. If the result would pass the maximum state size (100,000 basis amplitudes, from getMaxStateSize()), the interaction throws [QuantumForgeStateSizeError] without allocating:
[QuantumForgeStateSizeError] Tensor product would produce ~177147 basis states, exceeding limit of 100000 (entangling 1 + 10 qudits)On the Qutrit Edition this can happen before the qudit limit: a group of fully superposed qutrits passes 100,000 amplitudes at 11 qudits. When the guard fires, both properties stay in their own states.
The same advice applies as for the qudit limit: check before the interaction rather than catching. State budget queries give you what you need:
import { getMaxStateSize } from "quantum-forge/quantum";
if (a.stateVectorSize() * b.stateVectorSize() > getMaxStateSize()) {
fallbackToClassical(); // the interaction could pass the limit
} else {
a.iSwap(b, 0.5);
}The product is an upper bound: if a and b already share a state, no tensor product happens at all.
Graceful degradation
Treat quantum as an enhancement. When the quantum state has no room for an interaction, fall back to classical behavior and keep the game playable:
import { getMaxQudits, quantum, type Quantum } from "quantum-forge/quantum";
interface Ball {
exists?: Quantum<boolean>;
}
/** Split a ball into an entangled pair, or skip if the quantum budget is spent. */
function trySplit(original: Ball, clone: Ball): boolean {
if (!original.exists) return false;
if (original.exists.numActiveQudits() >= getMaxQudits()) return false;
const split = quantum([false, true]);
original.exists.iSwap(split, 0.5);
clone.exists = split;
return true;
}When the budget is spent, the split is skipped and the game carries on with a classical ball.
Raw properties and QuantumSimulation
Code that works on raw WASM properties, through QuantumSimulation or handle.raw and the batch APIs, gets the simulator's own errors unfiltered.
A gate across two simulations throws [QuantumForgeError] Cannot entangle properties from different QuantumSimulation contexts. A predicate on the gate's own target throws a target/control overlap, whose message reads like Cycle operation has overlapping targets and controls.
Destroying a raw property that is still entangled, with sim.destroyProperty(prop) or prop.destroy(), measures it, which collapses its partners, and prints a console warning:
[QuantumForge] Warning: destroying entangled property (qudit 2 in 4-qudit shared state).
Measurement will collapse entangled partners.The operation still succeeds. If you see the warning unexpectedly, an undo step was probably missed: reverse the operations that created the entanglement, for example i_swap(prop, ancilla, -0.5) after i_swap(prop, ancilla, 0.5), before destroying.
Never call destroy() on handle.raw. The handle owns that property, and dispose() fails after one. See Lifecycle: the raw WASM property.