Inspired by BulletProof React, I applied its codebase architecture concepts to the Umami codebase.
This article focuses only on the error handling strategies used in Umami codebase.
Prerequisite
- Error handling in Umami codebase — Part 1.0
Approach
In part 1.0, I mentioned that Bulletproof React mentions three ways of error handling:
API errors
In app errors
Error tracking
In this article, I want to provide examples and references to how Umami handled in-app errors. The approach we take is simple:
Pick common/DataGrid.tsx component that handles the error
Review how the error is handled
You see Umami uses Tanstack Query. So the way in app errors are handled is that it involves destructing the query result and then use the error variable.
const { data, error, isLoading, isFetching } = query;
In this part 1.0, we review the common/DataGrid.tsx.
DataGrid.tsx
This common/DataGrid.tsx component is used in quite some pages. Following are some examples showing how this DataGrid component is reused
Websites
In WebsitesDataTable.tsx, you will find the following code:
import Link from 'next/link';
import { DataGrid } from '@/components/common/DataGrid';
import { useLoginQuery, useNavigation, useUserWebsitesQuery } from '@/components/hooks';
import { Favicon } from '@/index';
import { Icon, Row } from '@umami/react-zen';
import { WebsitesTable } from './WebsitesTable';
export function WebsitesDataTable({
userId,
teamId,
allowEdit = true,
allowView = true,
showActions = true,
}: {
userId?: string;
teamId?: string;
allowEdit?: boolean;
allowView?: boolean;
showActions?: boolean;
}) {
const { user } = useLoginQuery();
const queryResult = useUserWebsitesQuery({ userId: userId || user?.id, teamId });
const { renderUrl } = useNavigation();
const renderLink = (row: any) => (
<Row alignItems="center" gap="3">
<Icon size="md" color="muted">
<Favicon domain={row.domain} />
</Icon>
<Link href={renderUrl(`/websites/${row.id}`, false)}>{row.name}</Link>
</Row>
);
return (
<DataGrid query={queryResult} allowSearch allowPaging>
{({ data }) => (
<WebsitesTable
data={data}
showActions={showActions}
allowEdit={allowEdit}
allowView={allowView}
renderLink={renderLink}
/>
)}
</DataGrid>
);
}
Here queryResult is entirely passed down as a prop to DataGrid.
Links
Similarly in the LinksDataTable.tsx, you will find the following code:
import { DataGrid } from '@/components/common/DataGrid';
import { useLinksQuery, useNavigation } from '@/components/hooks';
import { LinksTable } from './LinksTable';
export function LinksDataTable() {
const { teamId } = useNavigation();
const query = useLinksQuery({ teamId });
return (
<DataGrid
query={query}
allowSearch={true}
autoFocus={false}
allowPaging={true}
>
{({ data }) => <LinksTable data={data} />}
</DataGrid>
);
}
Do you see the pattern here? query results is passed down as a prop entirely to the DataGrid component.
DataGrid
Let’s take a look at this DataGrid component. It has about 107 LOC at the time of writing this article.
Query result is destructured as shown below:
export function DataGrid({
query,
searchDelay = 600,
allowSearch,
allowPaging = true,
autoFocus,
renderActions,
renderEmpty = () => <Empty />,
children,
}: DataGridProps) {
const { formatMessage, labels } = useMessages();
const { data, error, isLoading, isFetching } = query;
Then these variables are passed down as props to the component, LoadingPanel.
LoadingPanel
<LoadingPanel
data={data}
isLoading={isLoading}
isFetching={isFetching}
error={error}
renderEmpty={renderEmpty}
>
{data && (
<>
<Column>
{isValidElement(child)
? cloneElement(child as ReactElement<any>, { displayMode })
: child}
</Column>
{showPager && (
<Row marginTop="6">
<Pager
page={data.page}
pageSize={data.pageSize}
count={data.count}
onPageChange={handlePageChange}
/>
</Row>
)}
</>
)}
</LoadingPanel>
In the LoadingPanel, you will find the following code:
// Show loading spinner only if no data exists
if (isLoading || isFetching) {
return (
<Column position="relative" height="100%" width="100%" {...props}>
<Loading icon={loadingIcon} placement={loadingPlacement} />
</Column>
);
}
// Show error
if (error) {
return <ErrorMessage />;
}
There is other code snippets but I am more interested in learning how the In-App errors are handled. So if there is an error, ErrorMessage component is rendered and this is based on the destructured error value from the query result.
ErrorMessage
You will find the following code in ErrorMessage.tsx.
import { Icon, Row, Text } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import { AlertTriangle } from '@/components/icons';
export function ErrorMessage() {
const { formatMessage, messages } = useMessages();
return (
<Row alignItems="center" justifyContent="center" gap>
<Icon>
<AlertTriangle />
</Icon>
<Text>{formatMessage(messages.error)}</Text>
</Row>
);
}
About me:
Hey, my name is Ramu Narasinga. I study codebase architecture in large open-source projects.
Email: ramu.narasinga@gmail.com
I spent 200+ hours analyzing Supabase, shadcn/ui, LobeChat. Found the patterns that separate AI slop from production code. Stop refactoring AI slop. Start with proven patterns. Check out production-grade projects at thinkthroo.com

Top comments (0)