Skip to content

Commit f98e92a

Browse files
committed
chore: standardize error handling
1 parent e24cd8f commit f98e92a

File tree

10 files changed

+260
-111
lines changed

10 files changed

+260
-111
lines changed

webapp/src/app/(dashboard)/[organizationId]/members/members-list.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,10 @@ export default function MembersList({
4545

4646
await toast
4747
.promise(
48-
fetchApi(
49-
`/organizations/${organizationId}/add-user`,
50-
{
51-
method: "POST",
52-
body: body,
53-
},
54-
).then(async (result) => {
48+
fetchApi(`/organizations/${organizationId}/add-user`, {
49+
method: "POST",
50+
body: body,
51+
}).then(async (result) => {
5552
if (!result) {
5653
throw new Error("Failed to add user");
5754
}

webapp/src/app/(dashboard)/[organizationId]/page.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import {
1111
getEquivalentCitizenPercentage,
1212
getEquivalentTvTime,
1313
} from "@/helpers/constants";
14-
import { REFRESH_INTERVAL_ONE_MINUTE, THIRTY_DAYS_MS, SECONDS_PER_DAY } from "@/helpers/time-constants";
14+
import {
15+
REFRESH_INTERVAL_ONE_MINUTE,
16+
THIRTY_DAYS_MS,
17+
SECONDS_PER_DAY,
18+
} from "@/helpers/time-constants";
1519
import { fetcher } from "@/helpers/swr";
1620
import { getOrganizationEmissionsByProject } from "@/server-functions/organizations";
1721
import { Organization } from "@/types/organization";
@@ -87,7 +91,9 @@ export default function OrganizationPage({
8791
label: "days",
8892
value: organizationReport?.duration
8993
? parseFloat(
90-
(organizationReport.duration / SECONDS_PER_DAY).toFixed(2),
94+
(organizationReport.duration / SECONDS_PER_DAY).toFixed(
95+
2,
96+
),
9197
)
9298
: 0,
9399
},

webapp/src/components/date-range-picker.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ export function DateRangePicker({ date, onDateChange }: DateRangePickerProps) {
6969
defaultMonth={date?.from}
7070
selected={tempDateRange}
7171
onSelect={(range) => {
72-
console.log("onSelect called with:", range);
7372
setTempDateRange(range);
7473
}}
7574
numberOfMonths={2}

webapp/src/components/ui/calendar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ function Calendar({
5959
Chevron: ({ className, orientation, ...props }) => {
6060
const Icon =
6161
orientation === "left" ? ChevronLeft : ChevronRight;
62-
return <Icon className={cn("h-4 w-4", className)} {...props} />;
62+
return (
63+
<Icon className={cn("h-4 w-4", className)} {...props} />
64+
);
6365
},
6466
}}
6567
{...props}

webapp/src/helpers/dashboard-calculations.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ export function calculateRadialChartData(
4545
label: "days",
4646
value: parseFloat(
4747
report
48-
.reduce((n, { duration }) => n + duration / SECONDS_PER_DAY, 0)
48+
.reduce(
49+
(n, { duration }) => n + duration / SECONDS_PER_DAY,
50+
0,
51+
)
4952
.toFixed(2),
5053
),
5154
},

webapp/src/hooks/useProjectDashboard.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ export function useProjectDashboard(
114114

115115
setSelectedExperimentId(report[0]?.experiment_id ?? "");
116116

117-
const newConvertedValues = calculateConvertedValues(newRadialChartData);
117+
const newConvertedValues =
118+
calculateConvertedValues(newRadialChartData);
118119
setConvertedValues(newConvertedValues);
119120
},
120121
[date],
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Error Handling Standards for Server Functions
2+
3+
## Overview
4+
5+
This document defines the standard error handling patterns for all server-side API functions in the codebase.
6+
7+
## Core Principles
8+
9+
1. **User feedback comes from the UI layer** - Server functions focus on data retrieval/mutation
10+
2. **Consistent patterns** - Similar operations should handle errors the same way
11+
3. **Graceful degradation** - Read operations should fail gracefully with empty data
12+
4. **Clear failure signals** - Write operations should throw errors for the UI to catch
13+
14+
---
15+
16+
## Standard Patterns
17+
18+
### Pattern A: Read Operations (GET)
19+
20+
**Use for:** Fetching data, list operations, queries
21+
22+
```typescript
23+
export async function getData(id: string): Promise<Data[]> {
24+
const result = await fetchApi<Data[]>(`/endpoint/${id}`);
25+
26+
// Return empty array/null on failure - UI will show "no data" state
27+
if (!result) {
28+
return []; // or null for single items
29+
}
30+
31+
return result;
32+
}
33+
```
34+
35+
**Why:**
36+
37+
- Users can still use the app even if one data source fails
38+
- UI naturally shows "no data" or "empty" states
39+
- Errors are already logged by `fetchApi`
40+
41+
### Pattern B: Write Operations (POST/PUT/PATCH/DELETE)
42+
43+
**Use for:** Creating, updating, deleting data
44+
45+
```typescript
46+
export async function createData(data: Data): Promise<Data> {
47+
const result = await fetchApi<Data>("/endpoint", {
48+
method: "POST",
49+
body: JSON.stringify(data),
50+
});
51+
52+
// Throw error - UI will catch and show toast/error message
53+
if (!result) {
54+
throw new Error("Failed to create data");
55+
}
56+
57+
return result;
58+
}
59+
```
60+
61+
**Why:**
62+
63+
- Write operations are user-initiated actions that need feedback
64+
- UI layer can catch the error and show appropriate toast/modal
65+
- Clear signal that the operation failed
66+
67+
### Pattern C: Critical Read Operations
68+
69+
**Use for:** Data required for the page to function (rare)
70+
71+
```typescript
72+
export async function getCriticalData(id: string): Promise<Data> {
73+
const result = await fetchApi<Data>(`/critical/${id}`);
74+
75+
if (!result) {
76+
throw new Error("Failed to load required data");
77+
}
78+
79+
return result;
80+
}
81+
```
82+
83+
**Why:**
84+
85+
- Some data is essential for the page to work
86+
- UI can show error boundary or redirect
87+
88+
---
89+
90+
## Migration Guide
91+
92+
### ❌ Avoid Try-Catch in Server Functions
93+
94+
```typescript
95+
// DON'T DO THIS - fetchApi already handles errors
96+
try {
97+
const result = await fetchApi<T>(endpoint);
98+
return result || [];
99+
} catch (error) {
100+
console.error(error);
101+
return [];
102+
}
103+
```
104+
105+
```typescript
106+
// DO THIS - Let fetchApi handle errors, check result
107+
const result = await fetchApi<T>(endpoint);
108+
return result || [];
109+
```
110+
111+
### UI Layer Responsibilities
112+
113+
The UI components should handle errors from write operations:
114+
115+
```typescript
116+
// In React component
117+
const handleCreate = async () => {
118+
try {
119+
await createData(formData);
120+
toast.success("Created successfully");
121+
} catch (error) {
122+
toast.error(error.message || "Failed to create");
123+
}
124+
};
125+
```
126+
127+
---
128+
129+
## Examples by Function Type
130+
131+
| Function Type | Pattern | Return on Error | Example |
132+
| -------------------- | ------- | --------------- | ------------------ |
133+
| `getProjects()` | A | `[]` | List of projects |
134+
| `getOneProject()` | A | `null` | Single project |
135+
| `createProject()` | B | `throw` | Create new project |
136+
| `updateProject()` | B | `throw` | Update project |
137+
| `deleteProject()` | B | `void` (throws) | Delete project |
138+
| `getOrganizations()` | A | `[]` | List of orgs |
139+
| `getUserProfile()` | C | `throw` | Required for auth |
140+
141+
---
142+
143+
## Decision Tree
144+
145+
```
146+
Is this a READ operation?
147+
├─ Yes: Is the data critical for the page?
148+
│ ├─ Yes: Use Pattern C (throw)
149+
│ └─ No: Use Pattern A (return empty)
150+
└─ No (WRITE operation): Use Pattern B (throw)
151+
```

webapp/src/server-functions/organizations.ts

Lines changed: 30 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,76 +6,63 @@ import { fetchApiServer } from "@/helpers/api-server";
66
export async function getOrganizationEmissionsByProject(
77
organizationId: string,
88
dateRange: DateRange | undefined,
9-
): Promise<OrganizationReport | null> {
10-
try {
11-
let endpoint = `/organizations/${organizationId}/sums`;
9+
): Promise<OrganizationReport> {
10+
let endpoint = `/organizations/${organizationId}/sums`;
1211

13-
if (dateRange?.from && dateRange?.to) {
14-
endpoint += `?start_date=${dateRange.from.toISOString()}&end_date=${dateRange.to.toISOString()}`;
15-
}
16-
17-
const result = await fetchApiServer<OrganizationReport>(endpoint);
12+
if (dateRange?.from && dateRange?.to) {
13+
endpoint += `?start_date=${dateRange.from.toISOString()}&end_date=${dateRange.to.toISOString()}`;
14+
}
1815

19-
// Handle case when no emissions data is found
20-
if (!result) {
21-
// Return zeros for all metrics
22-
return {
23-
name: "",
24-
emissions: 0,
25-
energy_consumed: 0,
26-
duration: 0,
27-
};
28-
}
16+
const result = await fetchApiServer<OrganizationReport>(endpoint);
2917

30-
return result;
31-
} catch (error) {
32-
console.error("Error fetching organization emissions:", error);
33-
// Return default values if there's an error
18+
// Return empty data on failure (Pattern A - Read operation)
19+
if (!result) {
3420
return {
3521
name: "",
3622
emissions: 0,
3723
energy_consumed: 0,
3824
duration: 0,
3925
};
4026
}
27+
28+
return result;
4129
}
4230

4331
export async function getDefaultOrgId(): Promise<string | null> {
44-
try {
45-
const orgs = await fetchApiServer<Organization[]>("/organizations");
46-
if (!orgs) {
47-
return null;
48-
}
32+
const orgs = await fetchApiServer<Organization[]>("/organizations");
4933

50-
if (orgs.length > 0) {
51-
return orgs[0].id;
52-
}
53-
} catch (err) {
54-
console.warn("error processing organizations list", err);
34+
// Return null on failure (Pattern A - Read operation)
35+
if (!orgs || orgs.length === 0) {
36+
return null;
5537
}
56-
return null;
38+
39+
return orgs[0].id;
5740
}
5841

5942
export async function getOrganizations(): Promise<Organization[]> {
60-
try {
61-
const orgs = await fetchApiServer<Organization[]>("/organizations");
62-
if (!orgs) {
63-
return [];
64-
}
43+
const orgs = await fetchApiServer<Organization[]>("/organizations");
6544

66-
return orgs;
67-
} catch (err) {
68-
console.warn("error fetching organizations list", err);
45+
// Return empty array on failure (Pattern A - Read operation)
46+
if (!orgs) {
6947
return [];
7048
}
49+
50+
return orgs;
7151
}
7252

7353
export const createOrganization = async (organization: {
7454
name: string;
7555
description: string;
76-
}): Promise<Organization | null> => {
77-
return fetchApiServer<Organization>("/organizations", {
56+
}): Promise<Organization> => {
57+
const result = await fetchApiServer<Organization>("/organizations", {
7858
method: "POST",
7959
body: JSON.stringify(organization),
8060
});
61+
62+
// Throw on failure (Pattern B - Write operation)
63+
if (!result) {
64+
throw new Error("Failed to create organization");
65+
}
66+
67+
return result;
8168
};

webapp/src/server-functions/projects.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { fetchApiServer } from "@/helpers/api-server";
44
export const createProject = async (
55
organizationId: string,
66
project: { name: string; description: string },
7-
): Promise<Project | null> => {
7+
): Promise<Project> => {
88
const result = await fetchApiServer<Project>("/projects", {
99
method: "POST",
1010
body: JSON.stringify({
@@ -13,23 +13,28 @@ export const createProject = async (
1313
}),
1414
});
1515

16+
// Throw on failure (Pattern B - Write operation)
1617
if (!result) {
17-
return null;
18+
throw new Error("Failed to create project");
1819
}
20+
1921
return result;
2022
};
2123

2224
export const updateProject = async (
2325
projectId: string,
2426
project: ProjectInputs,
25-
): Promise<Project | null> => {
27+
): Promise<Project> => {
2628
const result = await fetchApiServer<Project>(`/projects/${projectId}`, {
2729
method: "PATCH",
2830
body: JSON.stringify(project),
2931
});
32+
33+
// Throw on failure (Pattern B - Write operation)
3034
if (!result) {
31-
return null;
35+
throw new Error("Failed to update project");
3236
}
37+
3338
return result;
3439
};
3540

0 commit comments

Comments
 (0)