Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/ImageEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
29 changes: 29 additions & 0 deletions test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ImageEditor image="img-a" onError={onError} />);
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(<ImageEditor image="img-b" onError={onError} />);
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(<ImageEditor image="img-c" onError={onError} />);
await flush();
expect(mockInstance.reset).toHaveBeenLastCalledWith('img-c');

consoleError.mockRestore();
});

it('waits for a pending reset before destroying on unmount', async () => {
const resetDeferred = defer<void>();
mockInstance.reset.mockImplementationOnce(() => resetDeferred.promise);
Expand Down
Loading