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