Inside a JavaScript catch block you decide the failure is not yours to handle. Compare `throw err;`, `throw new Error(err.message);`, and logging the error and returning — what does each leave the outer caller with?
answer
- same object versus new object
- identity carries the custom properties
- stack was captured at construction
- a new Error re-roots the trace at your catch
- swallowing turns failure into a wrong value
basics
~20 sA bare throw err rethrows the identical object, so its type, custom properties and original stack all survive. throw new Error(err.message) starts a fresh error and discards them. Logging and returning is worst: the caller sees a successful call and a bogus value.
solid answer
~60 s`throw err;` rethrows the very same object — same identity, same `name`, same custom properties like `err.code`, and the same `stack`, because a stack is captured when the Error is constructed and is not rewritten by a rethrow. That is the lossless option, and it is what you want when the failure is genuinely not yours. `throw new Error(err.message)` constructs a *new* error: the outer caller loses the original type, any structured properties it carried, and the stack that pointed at the real origin — the new stack starts at your catch block. Wrapping is legitimate when you are deliberately translating a failure at an abstraction boundary, but only if you keep the original attached (`new Error(msg, { cause: err })`, ES2022). Catching, logging and returning is the dangerous one: the caller is told the call succeeded, and the bug resurfaces later as a null-ish value with no connection to its cause. The practical pattern is a conditional rethrow — handle only what you recognise, `throw err` for everything else.
code
javascript · 31 linesfunction load() {
const err = new Error('user 7 missing');
err.code = 'NOT_FOUND';
err.status = 404;
throw err;
}
function conditionalRethrow() {
try {
load();
} catch (err) {
if (err.code !== 'NOT_FOUND') throw err; // foreign failures pass through untouched
return null;
}
}
function lossyWrap() {
try {
load();
} catch (err) {
throw new Error(err.message); // code, status and the original stack are gone
}
}
console.log(conditionalRethrow()); // null
try {
lossyWrap();
} catch (err) {
console.log(err.name, err.code, err.status); // "Error" undefined undefined
}go deeper
Know that throw err; inside a catch passes the same error on unchanged, and that catching an error and returning null hides a real failure from the caller. Be able to write a catch that handles one known case and rethrows the rest.
Explain what identity buys you: the same name, the same custom properties and the same construction-time stack. Show why throw new Error(err.message) re-roots the trace at your catch block, and use the ES2022 cause option when you deliberately wrap.
Argue about where a failure should stop. Decide which layer logs, which layers rethrow silently, and which boundary translates to caller-facing language — and be able to point at swallowed errors in a real incident as the reason a bug took hours to trace.
Set the convention: one logging boundary per request or job, non-destructive wrapping at module edges, and a documented rule for which errors a layer is allowed to handle. Make the loss of context a reviewable defect rather than a matter of taste.
## Three things a catch block can do, and what each costs Once you have caught something, you own the decision about what the outer caller learns. There are only three real options — rethrow as-is, rethrow something new, or stop the propagation — and they differ enormously in how much context survives. ### 1. `throw err;` — lossless rethrow A bare rethrow of the caught binding throws the **same object**. Object identity is preserved, which means: - `err.name` and any subclass identity survive, so outer `instanceof` checks still work; - custom properties survive — `err.code`, `err.status`, whatever your layer attached; - the `stack` survives, unchanged. That last point surprises people. In mainstream engines the stack string is captured **when the Error is constructed**, so a rethrow does not append the rethrow site and does not clear anything. A bare rethrow is therefore invisible in the trace — good for fidelity, mildly annoying when you want to know which layers a failure passed through. ```js try { parse(input); } catch (err) { if (err.code !== 'E_SYNTAX') throw err; // not mine — pass it on untouched return fallback(); } ``` This **conditional rethrow** is the everyday shape: recognise the narrow case you can actually handle, and let everything else past. The alternative — catching broadly and handling generically — is how a `TypeError` from your own typo gets swallowed and reported as "parse failed". ### 2. `throw new Error(err.message);` — lossy wrap Here you construct a brand-new Error. The outer caller receives: - `name === 'Error'` — the original type is gone, so `instanceof MyDomainError` upstream is now false; - no `err.code`, no `err.status` — the properties that made the failure actionable are gone; - a stack rooted at *your catch block*, so the trace points at the translation layer instead of the code that actually failed. And the original object is unreachable: nothing in the language links the new error to it automatically. Wrapping is not wrong in itself. Translating a low-level failure into a boundary-appropriate one is a legitimate design move. What makes it a defect is doing it **destructively**. Since ES2022 the constructor takes an options bag with `cause`: ```js catch (err) { throw new Error('could not load user profile', { cause: err }); } ``` Now the caller gets a message in its own vocabulary *and* can reach the original through `err.cause`. A related trap is `throw new Error(err)` — passing the error itself as the message. The Error constructor converts its first argument to a string, so the message becomes `'Error: original message'`: a duplicated prefix, and everything except the message text discarded. ### 3. Log and return — swallowing ```js catch (err) { console.error(err); return null; // caller believes the call succeeded } ``` This is the most damaging of the three, because it converts a loud failure into a quiet wrong answer. The caller's control flow continues down the success path with a null-ish value; the real symptom appears somewhere else entirely, minutes or screens later, with no link back to the log line. Any check upstream that would have retried, fallen back, or surfaced a message to the user never runs. Swallowing is defensible in exactly one situation: you have genuinely handled the failure — the fallback value is a *correct* result and not a placeholder for "it broke". If a reader cannot tell those two apart from the code, the catch block is a bug. ### A fourth mistake: catch, log, rethrow ```js catch (err) { console.error(err); // logged here throw err; // ...and again at every layer above } ``` This preserves everything, so it is not lossy — it is noisy. Every layer that does it multiplies the log lines for one incident, and the log with the most stack frames is not necessarily the one anyone reads first. Log **once**, at the boundary where the failure stops propagating, and rethrow silently in between. ## The decision rule Ask what the caller can do with what you are about to give it: - **Can't help, can't add context** → `throw err;` - **Can add context that matters at this boundary** → `throw new Error(context, { cause: err });` - **Genuinely handled it, and the value returned is correct** → return, and say so in a comment or the function's name. Everything else — flattening errors into strings, catching broadly to "be safe", returning `null` on failure — trades away the only information anyone will have at 3am.
- Does re-throwing a caught error with `throw err;` add the rethrow location to its stack?No. Mainstream engines capture the stack string when the Error object is constructed, so a rethrow neither appends frames nor clears them — the trace still points at the original construction site. That is why a bare rethrow is perfectly lossless, and also why you cannot tell from the stack alone which layers re-threw a failure on its way up.
- When is wrapping a caught error better than just rethrowing it?When you are crossing an abstraction boundary and the original message would be meaningless — or leaky — to the caller. A repository turning a driver-level failure into 'could not load user profile' gives the caller something it can act on. Do it non-destructively with `new Error(message, { cause: err })` so the original is still reachable for diagnostics.
- What is wrong with the common shorthand `throw new Error(err)`?The Error constructor converts its first argument to a string, so passing an Error produces the message 'Error: original message' — a duplicated prefix — while discarding its type, its properties and its stack. If you want the original preserved, pass it as `{ cause: err }` and write a real message of your own.
- Every layer in our stack catches, logs and rethrows. What is the problem?Nothing is lost, but one incident produces a log entry per layer, and none of them is obviously the authoritative one. Log once — at the boundary where the failure actually stops, such as a request handler or a top-level entry point — and let intermediate layers rethrow silently or add context via `cause`.
saying these in an interview costs you the question
- Says rethrowing appends the current line to the stack
- Wraps every caught error in a fresh Error with just the message
- Catches broadly and returns null to keep things running
- Logs and rethrows at every layer of the stack
- Thinks the original error is linked to the new one automatically