From 90a878931d4d672c9e5e82fb97cc60cd34a448d5 Mon Sep 17 00:00:00 2001 From: galiprandi <20272796+galiprandi@users.noreply.github.com> Date: Sun, 21 Jun 2026 04:42:32 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Quality:=20Fix=20synchronous=20erro?= =?UTF-8?q?r=20handling=20in=20AsyncBlock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wrap `promiseFn` call in a `try...catch` block to handle synchronous exceptions. - Add unit test to verify synchronous error handling and prevent regressions. - Ensure component correctly transitions to error state and calls `onError`. - Maintain synchronous initial call to avoid micro-task delays in tests. --- .axioma/quality.md | 4 ++++ lib/components/AsyncBlock/index.test.tsx | 21 +++++++++++++++++++++ lib/components/AsyncBlock/index.tsx | 17 +++++++++-------- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/.axioma/quality.md b/.axioma/quality.md index 5e71f49..744c44a 100644 --- a/.axioma/quality.md +++ b/.axioma/quality.md @@ -101,3 +101,7 @@ ## 2024-07-10 - [Initial Mount Optimization and Robust Input Validation in Hooks] **Learning:** React hooks managing timing-sensitive side effects (like `useDebounce` or `useThrottle`) should use an `isFirstRun` ref to skip redundant `setTimeout` calls on initial mount, as the state is already initialized with the starting value. Additionally, implementing defensive checks for numeric parameters (e.g., `delay <= 0` or `NaN`) ensures the hook falls back to an immediate update, providing a safe and predictable behavior across different edge cases. **Action:** Always use an `isFirstRun` guard for initialization-safe side effects and validate numeric inputs to provide immediate fallbacks for invalid or non-positive values. + +## 2024-07-15 - [Synchronous Error Handling in Async Components] +**Learning:** Wrapping a function call in `Promise.resolve().then()` introduces a micro-task delay. This can break tests that expect the initial state of a component (like `pending`) to be set synchronously upon mount. For components managing async operations, using a `try...catch` around the initial function call and converting synchronous errors into a rejected promise (`promise = Promise.reject(e)`) ensures both sync and async errors are handled by the same pipeline without breaking synchronous test expectations. +**Action:** Use `try...catch` around initial calls to functions expected to return promises to robustly handle synchronous exceptions without introducing micro-task delays. diff --git a/lib/components/AsyncBlock/index.test.tsx b/lib/components/AsyncBlock/index.test.tsx index 5c77a47..110bd3d 100644 --- a/lib/components/AsyncBlock/index.test.tsx +++ b/lib/components/AsyncBlock/index.test.tsx @@ -476,4 +476,25 @@ describe('', () => { expect(onError).not.toHaveBeenCalled() }) + + it('should handle synchronous errors in promiseFn', async () => { + const error = new Error('Sync error') + const onError = vi.fn() + const errorContent = 'Error' + + render( + { + throw error + }} + pending="Loading" + success={() => null} + error={() => errorContent} + onError={onError} + />, + ) + + await screen.findByText(errorContent) + expect(onError).toHaveBeenCalledWith(error) + }) }) diff --git a/lib/components/AsyncBlock/index.tsx b/lib/components/AsyncBlock/index.tsx index 390e22b..fd1b20e 100644 --- a/lib/components/AsyncBlock/index.tsx +++ b/lib/components/AsyncBlock/index.tsx @@ -1,10 +1,4 @@ -import { - useEffect, - useState, - ReactNode, - useRef, - useCallback, -} from 'react' +import { useEffect, useState, ReactNode, useRef, useCallback } from 'react' /** * AsyncBlock component – Declaratively renders asynchronous content with pending, success, and error states. @@ -94,7 +88,14 @@ export const AsyncBlock = ({ } // Pass the signal to the promise function, but make it optional - promiseFn(signal) + let promise: Promise + try { + promise = promiseFn(signal) + } catch (e) { + promise = Promise.reject(e) + } + + promise .then((result) => { if (signal.aborted) return