Skip to content

Commit cb46b55

Browse files
committed
chore: adding reset to an InternalError as well as cleaning up error messages
1 parent aedcfdc commit cb46b55

File tree

6 files changed

+65
-21
lines changed

6 files changed

+65
-21
lines changed

docs/04/01/error-handling.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Error Handling in the React SDK
2+
3+
Error handling in the React SDK occurs across multiple layers:
4+
5+
1. **Top-Level Error Boundary** — in `GustoProviderCustomUIAdapter.tsx`
6+
2. **Component-Level Error Boundary** — in `Base.tsx`
7+
3. **Error Processing Logic** — in `Base.tsx`
8+
9+
---
10+
11+
## Known Errors
12+
13+
The `Base` component handles three main categories of known errors:
14+
15+
- **API client errors** caused by Zod validation failures on request or response schemas
16+
- **Explicitly set errors** from descendants of the `Base` component
17+
- **Errors returned directly by the API**
18+
19+
These known errors are rendered alongside the component’s children at the top level of `Base.tsx`. They differ in structure and are displayed with context-specific UI.
20+
21+
---
22+
23+
## Catastrophic Errors
24+
25+
Unexpected errors thrown by descendants of the `Base` component are caught by the error boundary in `Base.tsx` and rendered using the `InternalError` component or a custom `FallbackComponent` if provided. These are considered **catastrophic errors**, meaning the component cannot render itself.
26+
27+
In such cases, a **"Try again"** button is shown, which allows users to attempt a re-render of the component.
28+
29+
---
30+
31+
## Unrecognized Errors
32+
33+
Errors that are caught by `Base.tsx` but not recognized as known error types are re-thrown. These are then handled by the top-level error boundary in `GustoProviderCustomUIAdapter.tsx`, unless a partner provides additional error boundaries between `GustoProvider` and t

docs/04/integration-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ The integration guide will provide general concepts for building with the Gusto
99
| Providing Your Own Data | How to supply your own data to SDK forms |
1010
| Translation | How to customize copy within SDK components and support for internationalization |
1111
| Routing | Example of integrating the SDK with your application router |
12+
| Error Handling | How does SDK handle internal errors router |

src/components/Base/Base.tsx

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { APIError } from '@gusto/embedded-api/models/errors/apierror'
77
import { SDKValidationError } from '@gusto/embedded-api/models/errors/sdkvalidationerror'
88
import { UnprocessableEntityErrorObject } from '@gusto/embedded-api/models/errors/unprocessableentityerrorobject'
99
import type { EntityErrorObject } from '@gusto/embedded-api/models/components/entityerrorobject'
10+
import { QueryErrorResetBoundary } from '@tanstack/react-query'
1011
import { FadeIn } from '../Common/FadeIn/FadeIn'
1112
import { BaseContext, type FieldError, type KnownErrors, type OnEventType } from './useBase'
1213
import { componentEvents, type EventType } from '@/shared/constants'
@@ -31,7 +32,7 @@ export interface BaseComponentInterface<TResourceKey extends keyof Resources = k
3132
}
3233

3334
/**Traverses errorList and finds items with message properties */
34-
const renderErrorList = (errorList: FieldError[]): React.ReactNode => {
35+
const renderErrorList = (errorList: FieldError[]): React.ReactNode[] => {
3536
return errorList.map(errorFromList => {
3637
if (errorFromList.message) {
3738
return <li key={errorFromList.key}>{errorFromList.message}</li>
@@ -91,7 +92,7 @@ export const BaseComponent = <TResourceKey extends keyof Resources = keyof Resou
9192
const processError = (error: KnownErrors) => {
9293
setError(error)
9394
//422 application/json - content relaited error
94-
if (error instanceof UnprocessableEntityErrorObject && Array.isArray(error.errors)) {
95+
if (error instanceof UnprocessableEntityErrorObject) {
9596
setFieldErrors(error.errors.flatMap(err => getFieldErrors(err)))
9697
}
9798
}
@@ -125,21 +126,32 @@ export const BaseComponent = <TResourceKey extends keyof Resources = keyof Resou
125126
baseSubmitHandler,
126127
}}
127128
>
128-
<ErrorBoundary
129-
FallbackComponent={FallbackComponent}
130-
onError={err => {
131-
onEvent(componentEvents.ERROR, err)
132-
}}
133-
>
134-
{(error || fieldErrors) && (
135-
<Components.Alert label={t('status.errorEncountered')} status="error">
136-
{fieldErrors && <ul>{renderErrorList(fieldErrors)}</ul>}
137-
</Components.Alert>
129+
<QueryErrorResetBoundary>
130+
{({ reset: resetQueries }) => (
131+
<ErrorBoundary
132+
FallbackComponent={FallbackComponent}
133+
onReset={resetQueries}
134+
onError={err => {
135+
onEvent(componentEvents.ERROR, err)
136+
}}
137+
>
138+
{(error || fieldErrors) && (
139+
<Components.Alert label={t('status.errorEncountered')} status="error">
140+
{fieldErrors && <Components.UnorderedList items={renderErrorList(fieldErrors)} />}
141+
{error && error instanceof APIError && (
142+
<Components.Text>{error.message}</Components.Text>
143+
)}
144+
{error && error instanceof SDKValidationError && (
145+
<Components.Text as="pre">{error.pretty()}</Components.Text>
146+
)}
147+
</Components.Alert>
148+
)}
149+
<Suspense fallback={<LoaderComponent />}>
150+
<FadeIn>{children}</FadeIn>
151+
</Suspense>
152+
</ErrorBoundary>
138153
)}
139-
<Suspense fallback={<LoaderComponent />}>
140-
<FadeIn>{children}</FadeIn>
141-
</Suspense>
142-
</ErrorBoundary>
154+
</QueryErrorResetBoundary>
143155
</BaseContext.Provider>
144156
)
145157
}

src/components/Common/InternalError/InternalError.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import styles from './InternalError.module.scss'
44
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
55

66
export const InternalError = ({ error, resetErrorBoundary }: FallbackProps) => {
7-
//TODO: Need to integrate useQueryErrorResetBoundary from tanstac to reset query cach on "try again" - GWS-3926
87
const { t } = useTranslation('common')
98
const Components = useComponentContext()
109
const errorMessage =
@@ -13,7 +12,6 @@ export const InternalError = ({ error, resetErrorBoundary }: FallbackProps) => {
1312
: error instanceof Error
1413
? error.message
1514
: t('errors.unknownError')
16-
1715
return (
1816
<div className={styles.internalErrorCard} role="alert" data-testid="internal-error-card">
1917
<div>

src/components/Common/UI/Text/TextTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export interface TextProps extends Pick<HTMLAttributes<HTMLParagraphElement>, 'c
44
/**
55
* HTML element to render the text as
66
*/
7-
as?: 'p' | 'span' | 'div'
7+
as?: 'p' | 'span' | 'div' | 'pre'
88
/**
99
* Size variant of the text
1010
*/

src/components/Contractor/PaymentMethod/PaymentMethod.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ function Root({ contractorId, className, dictionary }: PaymentMethodProps) {
8686
})
8787

8888
const watchedType = useWatch({ control: formMethods.control, name: 'type' })
89-
9089
const onSubmit: SubmitHandler<PaymentMethodSchemaInputs> = async data => {
9190
await baseSubmitHandler(data, async payload => {
9291
let permitBankSubmission = true
92+
9393
if (payload.type === PAYMENT_METHODS.directDeposit) {
9494
/** Custom validation logic for accountNumber - because masked account value is used as default value, it is only validated when any of the bank-related fields are modified*/
9595
const { name, accountNumber, routingNumber, accountType } = payload
@@ -128,7 +128,7 @@ function Root({ contractorId, className, dictionary }: PaymentMethodProps) {
128128
contractorUuid: contractorId,
129129
requestBody: {
130130
name: payload.name,
131-
routingNumber: payload.routingNumber,
131+
routingNumber: '654789878',
132132
accountNumber: payload.accountNumber,
133133
accountType: payload.accountType,
134134
},

0 commit comments

Comments
 (0)