Skip to content

Commit bd2ccbd

Browse files
committed
refactor(core): extract logAndEmitError helper to deduplicate error handling
Consolidate the repeated console.error + emitError + .catch pattern into a single private logAndEmitError method on CopilotKitCore. All 4 call sites (setDefaultThrottleMs, subscribeToAgentWithOptions validation, safeCall reportError, unsupported-keys warning) now go through the helper.
1 parent b8d6e14 commit bd2ccbd

3 files changed

Lines changed: 57 additions & 49 deletions

File tree

packages/core/src/__tests__/core-subscribe-to-agent.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ describe("CopilotKitCore.subscribeToAgentWithOptions", () => {
553553
expect(onMessages).toHaveBeenCalledTimes(2);
554554
expect(errorSpy).toHaveBeenCalledWith(
555555
expect.stringContaining("must be a non-negative finite number"),
556+
expect.any(Error),
556557
);
557558

558559
errorSpy.mockRestore();
@@ -957,6 +958,7 @@ describe("CopilotKitCore.subscribeToAgentWithOptions", () => {
957958
expect(onEvent).not.toHaveBeenCalled();
958959
expect(warnSpy).toHaveBeenCalledWith(
959960
expect.stringContaining('callback "onEvent" is not supported'),
961+
expect.any(Error),
960962
);
961963

962964
warnSpy.mockRestore();

packages/core/src/core/core.ts

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,25 @@ export class CopilotKitCore {
409409
);
410410
}
411411

412+
/**
413+
* Log a message to the console and emit an error to subscribers.
414+
* Catches failures from `emitError` itself to prevent unhandled rejections.
415+
*/
416+
private logAndEmitError(
417+
message: string,
418+
params: {
419+
error: Error;
420+
code: CopilotKitCoreErrorCode;
421+
context?: Record<string, any>;
422+
},
423+
logLevel: "error" | "warn" = "error",
424+
): void {
425+
console[logLevel](message, params.error);
426+
this.emitError(params).catch((emitErr: unknown) => {
427+
console.error(message + " — emitError itself failed:", emitErr);
428+
});
429+
}
430+
412431
/**
413432
* Snapshot accessors
414433
*/
@@ -474,22 +493,17 @@ export class CopilotKitCore {
474493
*/
475494
setDefaultThrottleMs(value: number | undefined): void {
476495
if (value !== undefined && (!Number.isFinite(value) || value < 0)) {
477-
console.error(
496+
this.logAndEmitError(
478497
`CopilotKitCore.setDefaultThrottleMs: value must be a non-negative finite number or undefined, ` +
479498
`got ${value}. Keeping current value (${this._defaultThrottleMs}).`,
499+
{
500+
error: new Error(
501+
`setDefaultThrottleMs: invalid value (${value}), keeping current value (${this._defaultThrottleMs})`,
502+
),
503+
code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
504+
context: { value, currentValue: this._defaultThrottleMs },
505+
},
480506
);
481-
this.emitError({
482-
error: new Error(
483-
`setDefaultThrottleMs: invalid value (${value}), keeping current value (${this._defaultThrottleMs})`,
484-
),
485-
code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
486-
context: { value, currentValue: this._defaultThrottleMs },
487-
}).catch((emitErr: unknown) => {
488-
console.error(
489-
`CopilotKitCore.setDefaultThrottleMs: emitError itself failed:`,
490-
emitErr,
491-
);
492-
});
493507
return;
494508
}
495509
this._defaultThrottleMs = value;
@@ -677,22 +691,17 @@ export class CopilotKitCore {
677691
if (!Number.isFinite(resolved) || resolved < 0) {
678692
const source =
679693
options?.throttleMs !== undefined ? "throttleMs" : "defaultThrottleMs";
680-
console.error(
694+
this.logAndEmitError(
681695
`CopilotKitCore.subscribeToAgentWithOptions: ${source} must be a non-negative finite number, ` +
682696
`got ${resolved}. Falling back to unthrottled.`,
697+
{
698+
error: new Error(
699+
`subscribeToAgentWithOptions: invalid ${source} (${resolved}), falling back to unthrottled`,
700+
),
701+
code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
702+
context: { agentId: agent.agentId, source, value: resolved },
703+
},
683704
);
684-
this.emitError({
685-
error: new Error(
686-
`subscribeToAgentWithOptions: invalid ${source} (${resolved}), falling back to unthrottled`,
687-
),
688-
code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
689-
context: { agentId: agent.agentId, source, value: resolved },
690-
}).catch((emitErr: unknown) => {
691-
console.error(
692-
`CopilotKitCore.subscribeToAgentWithOptions[${agent.agentId || "(unknown agent)"}]: emitError itself failed:`,
693-
emitErr,
694-
);
695-
});
696705
} else {
697706
effectiveMs = resolved;
698707
}
@@ -709,18 +718,14 @@ export class CopilotKitCore {
709718
...args: Parameters<F>
710719
): any => {
711720
const reportError = (err: unknown, verb: string) => {
712-
const message = `CopilotKitCore.subscribeToAgentWithOptions[${agentLabel}]: ${label} callback ${verb}:`;
713-
console.error(message, err);
714-
this.emitError({
715-
error: err instanceof Error ? err : new Error(String(err)),
716-
code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
717-
context: { agentId: agent.agentId, callback: label },
718-
}).catch((emitErr: unknown) => {
719-
console.error(
720-
`CopilotKitCore.subscribeToAgentWithOptions[${agentLabel}]: emitError itself failed:`,
721-
emitErr,
722-
);
723-
});
721+
this.logAndEmitError(
722+
`CopilotKitCore.subscribeToAgentWithOptions[${agentLabel}]: ${label} callback ${verb}:`,
723+
{
724+
error: err instanceof Error ? err : new Error(String(err)),
725+
code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
726+
context: { agentId: agent.agentId, callback: label },
727+
},
728+
);
724729
};
725730
try {
726731
const result = fn(...args);
@@ -781,17 +786,15 @@ export class CopilotKitCore {
781786
`CopilotKitCore.subscribeToAgentWithOptions[${agentLabel}]: callback "${key}" is not supported ` +
782787
`and was dropped. Supported callbacks: ${Array.from(ALLOWED_KEYS).join(", ")}. ` +
783788
`Use agent.subscribe() directly for event handlers and per-item notifications.`;
784-
console.warn(message);
785-
this.emitError({
786-
error: new Error(message),
787-
code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
788-
context: { agentId: agent.agentId, droppedCallback: key },
789-
}).catch((emitErr: unknown) => {
790-
console.error(
791-
`CopilotKitCore.subscribeToAgentWithOptions[${agentLabel}]: emitError itself failed:`,
792-
emitErr,
793-
);
794-
});
789+
this.logAndEmitError(
790+
message,
791+
{
792+
error: new Error(message),
793+
code: CopilotKitCoreErrorCode.SUBSCRIBER_CALLBACK_FAILED,
794+
context: { agentId: agent.agentId, droppedCallback: key },
795+
},
796+
"warn",
797+
);
795798
}
796799
}
797800

packages/react-core/src/v2/hooks/__tests__/use-agent-throttle.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ describe("useAgent throttleMs", () => {
439439
expect.stringContaining(
440440
"throttleMs must be a non-negative finite number",
441441
),
442+
expect.any(Error),
442443
);
443444

444445
// Should behave as unthrottled — every notification fires immediately
@@ -898,6 +899,7 @@ describe("useAgent defaultThrottleMs from provider", () => {
898899
// The core setter rejects invalid values and logs an error
899900
expect(errorSpy).toHaveBeenCalledWith(
900901
expect.stringContaining("must be a non-negative finite number"),
902+
expect.any(Error),
901903
);
902904

903905
// Should behave as unthrottled (setter rejected the value)
@@ -1019,6 +1021,7 @@ describe("CopilotKitCore.setDefaultThrottleMs", () => {
10191021
expect(core.defaultThrottleMs).toBe(200);
10201022
expect(errorSpy).toHaveBeenCalledWith(
10211023
expect.stringContaining("must be a non-negative finite number"),
1024+
expect.any(Error),
10221025
);
10231026
errorSpy.mockRestore();
10241027
},

0 commit comments

Comments
 (0)