Q119: Error Objects, Custom Errors and Stack Traces
What Interviewers Want To Evaluate
This question checks whether you understand error modeling in JavaScript. Interviewers want Error, throwing values, custom error classes, cause, stack traces, async stacks, operational vs programmer errors, user-facing messages, and logging boundaries.
Senior answers should connect error design to debugging, observability, retries, and safe UX.
Short Interview Answer
JavaScript can throw any value, but production code should throw Error objects or subclasses because they carry messages, names, stacks, and optional causes. Custom errors help classify failures such as validation, network, authorization, or invariant errors. Stack traces show where errors were created or thrown, though async boundaries can make them harder to follow. Senior error handling separates user-facing messages from developer diagnostics and preserves context without leaking sensitive data.
Detailed Interview Answer
The base error type is Error.
throw new Error("Something failed");
An Error object commonly includes:
name
message
stack
cause
The exact stack format is engine-specific, but it is essential for debugging.
Throwing Non-Errors
JavaScript allows this:
throw "failed";
But it is a poor production practice because strings do not carry structured stack and classification behavior.
Prefer:
throw new Error("Failed to load profile");
Custom Errors
Custom error classes classify failures.
class ValidationError extends Error {
constructor(message, details) {
super(message);
this.name = "ValidationError";
this.details = details;
}
}
This lets callers distinguish expected validation failures from unexpected system failures.
Error Cause
Modern JavaScript supports cause.
try {
await fetchProfile();
} catch (error) {
throw new Error("Could not load profile", {
cause: error,
});
}
cause preserves lower-level context while adding higher-level meaning.
Operational vs Programmer Errors
A useful production distinction:
operational error: expected external failure, such as network timeout
programmer error: bug, invalid invariant, impossible state
Operational errors may be recoverable. Programmer errors often need visibility, rollback, or code fixes.
User-Facing vs Developer Messages
Do not show raw internal error messages to users.
catch (error) {
logError(error);
showToast("We could not save your changes. Please try again.");
}
Logs should contain diagnostic details. UI should contain safe, helpful guidance.
Stack Traces
A stack trace shows the call path that led to an error.
function a() {
b();
}
function b() {
throw new Error("Broken");
}
The stack helps locate the source.
In production frontend apps, source maps are needed to map minified stack traces back to original source.
Async Stack Complexity
Async code can split stack traces across promise boundaries.
async function load() {
await fetchData();
throw new Error("Render failed");
}
Modern tools often improve async stack traces, but you should still preserve context with meaningful errors and logs.
Error Boundaries
In React, error boundaries catch render-time errors in component trees. They do not automatically catch event handler errors or async callback errors.
This means async failures need explicit handling.
button.addEventListener("click", () => {
save().catch(reportError);
});
Retryable Errors
Classifying errors helps retry logic.
class TimeoutError extends Error {
constructor(message) {
super(message);
this.name = "TimeoutError";
this.retryable = true;
}
}
Retries should be based on error type, idempotency, and user impact, not blind repetition.
Common Mistakes
A common mistake is catching an error and throwing a new one without preserving the original cause.
Another mistake is swallowing errors silently.
try {
await save();
} catch {}
Silent failure destroys debuggability and user trust.
Senior Trade-Offs
Not every error needs a custom class. Too many classes can create ceremony.
Use custom errors where classification changes behavior: retry, user messaging, monitoring severity, authorization handling, or form validation.
Code or Design Example
class ApiError extends Error {
constructor(message, { status, cause } = {}) {
super(message, { cause });
this.name = "ApiError";
this.status = status;
}
}
async function request(url) {
const response = await fetch(url);
if (!response.ok) {
throw new ApiError("API request failed", {
status: response.status,
});
}
return response.json();
}
This gives callers structured failure information.
Debugging Workflow
When error handling is poor:
check whether thrown values are Error objects
preserve cause when wrapping
classify recoverable vs unexpected failures
separate user messages from logs
verify source maps for production stacks
check async boundaries and event handlers
avoid empty catch blocks
add monitoring context without sensitive data
Interview Framing
Explain why throwing Error objects matters. Then discuss custom error classes, cause, stack traces, async boundaries, and user-safe messaging.
Revision Notes
Errors should preserve context, classification, and stack information. User messages and developer diagnostics are different outputs.
Key Takeaways
Good error design makes systems easier to recover, debug, monitor, and explain to users.
Follow-Up Questions
- Can JavaScript throw non-Error values?
- Why should you throw Error objects?
- What is a custom error class?
- What does
causepreserve? - What is a stack trace?
- Why are source maps important?
- Do React error boundaries catch async errors?
- What is an operational error?
- When should errors be retryable?
- Why are empty catch blocks dangerous?
How I Would Answer This In A Real Interview
I would say JavaScript can throw anything, but production code should throw Error objects. Then I would explain custom classes, cause, stack traces, async boundaries, and how senior systems separate safe user messaging from diagnostic logging.