Skip to content

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:

SituationMessage (N and M are handle ids)Fix
Any call before the module has loadedQuantumForge not loaded. Call ensureLoaded() first and await it.await ensureLoaded() at startup
quantum(1), or a single declared valuequantum(1): a quantum property needs at least 2 values. (a RangeError)Declare two or more values
The same value declared twicequantum(["a", "a"]): value "a" is declared twice.Make every value unique
More values than the build allowsquantum(): 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 valuesflip() 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 valuesY gate requires dimension 2 (qubit)y() is a qubit gate
swap() or iSwap() passed the handle it is called onswap(): quantum property #N cannot swap with itself.Pass a different property
swap() or iSwap() across different numbers of valuesiSwap(): 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 targetflip(): 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 infiniteflip fraction must be a finite number, got NaN. (a RangeError)Pass a finite number
Any call on a disposed handleQuantum property #N was disposed and can no longer be used.Stop using the handle after dispose()
A predicate built before its property was disposedPredicate on quantum property #N cannot be used: the property was disposed.Build predicates from live handles
Forcing an outcome with zero probabilityforcedMeasure(): 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:

PrefixMeaningCommon cause
[QuantumForgeStateSizeError]State vector would exceed the maximum sizeToo many entangled qudits; the tensor product would be too large
[QuantumForgeOutOfMemoryError]WASM memory allocation failedWASM heap exhausted
[QuantumForgeError]General quantum operation errorCross-simulation entanglement on raw properties

A helper keeps the matching readable:

typescript
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:

typescript
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:

typescript
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:

typescript
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.

Powered by Quantum Forge