diff --git a/src/ImageEditor.tsx b/src/ImageEditor.tsx
index 2151ee0..404763a 100644
--- a/src/ImageEditor.tsx
+++ b/src/ImageEditor.tsx
@@ -47,7 +47,17 @@ function ImageEditorInner(
const err = error instanceof Error ? error : new Error(String(error));
const { onError } = latestPropsRef.current;
if (onError) {
- onError(err);
+ // A throwing onError must never escape: fail() runs as the terminal
+ // .catch of the serialized chain, so a throw here would reject
+ // chainRef and poison every subsequent link (see the invariant above).
+ try {
+ onError(err);
+ } catch (callbackError) {
+ console.error(
+ '[react-image-editor] onError callback threw',
+ callbackError
+ );
+ }
} else {
console.error('[react-image-editor]', err);
}
diff --git a/test/index.test.tsx b/test/index.test.tsx
index ac1ec0e..2eaba8c 100644
--- a/test/index.test.tsx
+++ b/test/index.test.tsx
@@ -477,6 +477,35 @@ it('reports a reset rejection via onError without poisoning the chain', async ()
expect(mockInstance.reset).toHaveBeenLastCalledWith('img-c');
});
+it('contains a throwing onError without poisoning the chain', async () => {
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
+ const onError = vi.fn(() => {
+ throw new Error('boom');
+ });
+ const { rerender } = render();
+ await flush();
+
+ // fail() runs as the terminal .catch of the chain; a throwing onError
+ // must be swallowed rather than rejecting chainRef.
+ mockInstance.reset.mockRejectedValueOnce(new Error('reload failed'));
+ rerender();
+ await flush();
+
+ expect(onError).toHaveBeenCalledWith(expect.any(Error));
+ // The callback's own throw is reported, not propagated.
+ expect(consoleError).toHaveBeenCalledWith(
+ '[react-image-editor] onError callback threw',
+ expect.any(Error)
+ );
+
+ // The chain survived: a later image change still resets normally.
+ rerender();
+ await flush();
+ expect(mockInstance.reset).toHaveBeenLastCalledWith('img-c');
+
+ consoleError.mockRestore();
+});
+
it('waits for a pending reset before destroying on unmount', async () => {
const resetDeferred = defer();
mockInstance.reset.mockImplementationOnce(() => resetDeferred.promise);