Q214: Type Safe Error Modeling and Result Types
What Interviewers Want To Evaluate
Interviewers want to know whether you can design predictable error flows instead of throwing strings and hoping every caller catches correctly.
They check custom errors, discriminated error unions, result types, async errors, retryable failures, UI error states, logging, and when exceptions are still appropriate.
Short Interview Answer
I model expected failures as typed results or discriminated unions, especially for validation, permissions, network states, and recoverable user flows. Exceptions are still useful for unexpected programmer errors or framework boundaries. The important thing is that callers can distinguish error categories and decide whether to retry, show a field error, redirect, log, or fail safely.
Detailed Interview Answer
Errors are part of the API.
If error shapes are unclear, UI behavior becomes inconsistent.
Common frontend failures:
validation failed
network unavailable
unauthorized
forbidden
not found
rate limited
server unavailable
unexpected parsing issue
Each one may require different UX.
Throwing Exceptions
Exceptions are useful for unexpected failures.
function requireConfig(value: string | undefined) {
if (!value) {
throw new Error("Missing required config");
}
return value;
}
This is reasonable because the app cannot proceed correctly.
But for expected user-facing failures, typed results are often clearer.
Result Type
A result type makes success and failure explicit.
type Result<TData, TError> =
| { ok: true; data: TData }
| { ok: false; error: TError };
Usage:
type LoginError =
| { kind: "invalid_credentials"; message: string }
| { kind: "rate_limited"; retryAfterSeconds: number }
| { kind: "network"; message: string };
Then:
async function login(): Promise<Result<User, LoginError>> {
throw new Error("implementation omitted");
}
The caller must handle both branches.
UI Handling
function getLoginMessage(error: LoginError) {
switch (error.kind) {
case "invalid_credentials":
return error.message;
case "rate_limited":
return `Try again in ${error.retryAfterSeconds}s.`;
case "network":
return "Check your connection and try again.";
}
}
The discriminant tells the UI what happened.
Expected vs Unexpected
Separate expected failures from bugs.
Expected:
wrong password
invalid form field
permission denied
record not found
network timeout
Unexpected:
missing required config
impossible state reached
library invariant violated
unhandled code branch
malformed internal state
Expected failures should usually be modeled.
Unexpected failures can throw and be captured by an error boundary or logger.
Custom Error Classes
Custom classes can help when throwing.
class ConfigurationError extends Error {
constructor(message: string) {
super(message);
this.name = "ConfigurationError";
}
}
For cross-boundary errors, plain objects are often easier to serialize than classes.
Use classes when stack traces and instanceof checks are useful in one runtime.
Use discriminated objects for API responses and UI state.
Async Error Modeling
Async functions can fail in many ways.
type FetchError =
| { kind: "http"; status: number; message: string }
| { kind: "network"; message: string }
| { kind: "parse"; message: string };
Then:
type FetchResult<T> = Result<T, FetchError>;
This helps avoid catch blocks that treat every problem the same.
Retryable Errors
Add behavior-relevant fields.
type AppError =
| { kind: "network"; retryable: true; message: string }
| { kind: "validation"; retryable: false; fields: Record<string, string> }
| { kind: "auth"; retryable: false; redirectTo: "/login" };
The UI can decide:
show retry button
highlight fields
redirect to login
log silently
show fallback
React Error Boundaries
React error boundaries catch render errors.
They should not be the only error strategy.
Use them for unexpected UI crashes.
Use typed state for expected async outcomes.
type AsyncState<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: AppError };
This keeps normal user flows out of crash recovery.
Logging
Typed errors improve logging.
function logError(error: AppError) {
console.error({
kind: error.kind,
retryable: "retryable" in error ? error.retryable : false,
});
}
Logs should include enough context to debug without leaking secrets.
Common Mistakes
Common mistakes include:
throwing strings
catching unknown and assuming Error
using one generic message for every failure
mixing expected validation errors with crashes
forgetting that API errors must serialize
not modeling retryability or user action
The caller needs error information that maps to behavior.
Senior Trade-Offs
Result types add ceremony.
Use them for expected recoverable failures.
Exceptions are still fine for:
programmer errors
invariants
configuration failure
framework-level boundaries
unexpected impossible states
The design should make normal flows explicit and abnormal flows observable.
Interview Framing
In an interview, say:
I model expected failures as typed unions so UI behavior is explicit. I reserve exceptions for unexpected failures and make sure error boundaries and logs catch those paths.
Revision Notes
- Errors are part of an API contract.
- Result types make expected failures explicit.
- Discriminated error unions map errors to behavior.
- Exceptions are still useful for unexpected failures.
- API errors should be serializable.
- UI error states should not rely only on error boundaries.
Follow-Up Questions
- When would you use a result type?
- When are exceptions appropriate?
- How would you model retryable errors?
- Why should API errors be serializable?
- How do React error boundaries fit into error strategy?
How I Would Answer This In A Real Interview
I would say that typed error modeling helps the UI respond correctly. For expected failures like validation, auth, rate limits, and network problems, I prefer result types or discriminated unions. For impossible states or configuration failures, throwing is fine. I would make errors carry behavior-relevant data such as retryability, field messages, or redirect targets, and I would log unexpected failures through boundaries.