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