diff --git a/docs/docs/types/custom-structs.md b/docs/docs/types/custom-structs.md index 3cc2f2110..130849fe2 100644 --- a/docs/docs/types/custom-structs.md +++ b/docs/docs/types/custom-structs.md @@ -103,6 +103,25 @@ interface PartialPerson This way TypeScript always keeps the `interface` in-tact, allowing Nitrogen to properly process it. +## Cyclic references are not supported + +Direct or indirect cyclic struct references are not supported: + +```ts title="Direct cycle ❌" +interface Node { + child: Node +} +``` + +```ts title="Indirect cycle ❌" +interface A { + b: B +} +interface B { + a: A +} +``` + ## Structs are eagerly converted Since structs are just flat value types, each key/value is eagerly converted from a JS value to a native value (and vice-versa) when passing them between JS and native. diff --git a/packages/nitrogen/src/utils.ts b/packages/nitrogen/src/utils.ts index d841be374..b7199155c 100644 --- a/packages/nitrogen/src/utils.ts +++ b/packages/nitrogen/src/utils.ts @@ -35,6 +35,14 @@ export function errorToString(error: unknown): string { if (typeof error !== 'object') { return `${error}` } + if (error instanceof RangeError && error.message.includes('Maximum call stack size exceeded')) { + return ( + `${error.name}: ${error.message}\n` + + `This is likely caused by a direct or indirect cyclic struct reference ` + + `(e.g. \`interface Node { child: Node }\` or \`interface A { b: B }\` and \`interface B { a: A }\`).\n` + + `See: https://nitro.margelo.com/docs/types/custom-structs#cyclic-references-are-not-supported` + ) + } if (error instanceof Error) { let message = `${error.name}: ${error.message}` if (error.cause != null) {