Coding Style
This page outlines the coding style guidelines I use across projects. These aren’t rigid rules — just practical habits that help reduce bugs and improve clarity.
Error Handling & Logging Standards
Throw Low, Catch High
A foundational rule in error handling:
“Throw low” – detect and report errors where they happen. “Catch high” – log, handle, and show user-friendly messages where you have context.
- Low-level functions should only care about whether something worked or not.
- High-level code (like components or services) decides how to react.
Use a Consistent Result Object
Instead of throwing exceptions everywhere, return a predictable object:
{
success: boolean,
data?: any,
error?: {
code?: string,
message: string,
details?: any
}
}
This keeps your logic clean and avoids lots of try/catch blocks.
Keep Low-Level Errors Technical, High-Level Errors User-Friendly
- Low-level: technical reason – “Failed to fetch data”, “Parse error”.
- High-level: friendly message – “Something went wrong. Please try again.”
Example: Simple API Call Flow
Low-Level: Fetch Function
export const fetchUser = async (id) => {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
return {
success: false,
error: {
code: "NETWORK_ERROR",
message: "Failed to fetch user",
},
};
}
const data = await res.json();
return { success: true, data };
} catch (err) {
return {
success: false,
error: {
code: "UNKNOWN_ERROR",
message: "Unexpected error during fetch",
details: err,
},
};
}
};
What’s happening here:
- It never throws — it always returns a consistent result.
- Errors are short and technical.
- There’s a code that lets the caller decide how to respond.
High-Level: Where You Handle It
const result = await fetchUser(42);
if (!result.success) {
// Log for developers / monitoring
console.error("User fetch failed", result.error);
// Decide what the user should see
let message;
switch (result.error.code) {
case "NETWORK_ERROR":
message = "Couldn’t connect to the server. Please try again.";
break;
default:
message = "Something went wrong. Please try again.";
}
// Show notification
setNotification({ type: "error", message });
return;
}
// If successful, use the data
console.log("User:", result.data);
What’s happening here:
- The error is caught once at the top level.
- We log it for debugging.
- We show a friendly message to the user based on the code.
Summary
- Low level: keep errors technical and simple.
- High level: log them once and convert them into user-friendly messages.
- Always use a structured result object to avoid scattered try/catch.
- Use code to make branching logic predictable and clean.
Async Result Handling
Summary: You can destructure the result of an async call if it’s the only one in the scope. If you’re handling multiple results, use descriptive variable names that reflect the operation (like updateResult, titleResult).
Why: Destructuring is clean and convenient — but using it for multiple similar results in the same scope leads to ambiguity, overwrites, or hard-to-read code. Naming by operation keeps things clear and traceable.
Examples
Do (single result):
const { success, data, error } = await updateTask(...);
if (!success) {
console.error(error);
return;
}
return { success, data };
Do (multiple results):
const updateResult = await updateTask(...);
const titleResult = await generateTaskTitle(...);
if (!updateResult.success) {
console.error(updateResult.error);
return;
}
Avoid:
// Overlapping destructured names create confusion
const { success, error } = await updateTask(...);
const { success, data } = await generateTaskTitle(...); // bad!
// Generic result names don’t help
const result = await updateTask(...);
const result2 = await generateTaskTitle(...);