A container's dynamic invocation reports a generic failure whose stack trace ends in the reflection layer — how do you find the real fault?
answer
- two families, one exception type
- did the body run at all?
- no cause means no call happened
- classify on the cause, not the wrapper
- unwrap one layer, keep the chain
basics
~20 sAsk first whether the call happened at all: a failure with no cause attached means the machinery refused before the body ran, while a failure carrying a cause means the target ran and threw. Walk the cause chain to the root and classify on that, never on the wrapper.
solid answer
~40 sDynamic invocation produces two very different families of failure behind one exception type. The call may never have happened — no member with that name and signature, an argument that would not bind, a type that could not be instantiated, access refused — in which case the exception comes from the machinery and carries no cause from your code. Or the call happened and the target threw, in which case the invoker catches whatever came out and re-raises it inside a wrapper whose **cause** is the original. So the presence of a cause is itself the first bit of diagnosis: no cause means a wiring bug, a cause means an application bug. The invoker's own frames sit on top; the frames that matter are below the wrapper boundary.
code
pseudocode · 16 lines// what a caller usually writes
attempt {
invoke(target, "process", [order]) // process() raises OutOfStock
}
on failure matching OutOfStock {
compensate() // never runs
}
// what actually arrives
attempt {
invoke(target, "process", [order])
}
on failure e { // e is an InvocationFailure
root = e.cause // the OutOfStock raised inside process
if root matches OutOfStock then compensate() else rethrow(root)
}go deeper
Recall that a dynamic call reports its own failure type, and that the exception your code actually raised is stored inside it as the cause rather than being passed straight out.
Explain why the wrapping exists: one fixed invoking signature cannot declare an arbitrary target's failures, so the only faithful option is to capture the original and attach it as a cause.
Demonstrate the diagnosis under pressure: check for a cause first to separate wiring from logic, walk the chain rather than the top frames, and show how retry and alerting must key on the cause.
Set the boundary rule for the platform: which layer unwraps, what the container's two failure events are called, and how teams are stopped from writing handlers that silently never fire.
## Two families of failure behind one exception A dynamic call has two ways to go wrong, and they need opposite responses. **The call never happened.** No member is declared with that name and parameter types; the argument count is wrong; a value would not bind to a declared slot; the type has no constructor matching what was requested; access was refused. These are raised by the invocation machinery itself, *before* the target's body is entered. They describe your wiring, not your logic. **The call happened and the target threw.** The body ran, raised something, and that something has to get back to a caller whose call site is generic. | | The call never happened | The target ran and threw | |---|---|---| | Raised by | the invocation machinery | the target's own body | | Cause attached | typically none | the exception the target raised | | What it means | a wiring or manifest defect | an application defect | | Where to look | the manifest entry and the signature | the frames below the wrapper | | Retry sensible | almost never | sometimes, depending on the cause | ## Why the target's exception is wrapped The invoking API has one fixed signature and is compiled once, long before it knows which member it will call. It cannot declare the arbitrary set of failures an arbitrary target may raise, and in runtimes where failures are declared and checked it may not let an undeclared one escape. The only faithful option left is to catch whatever the target raised and re-raise it inside a wrapper, attaching the original as the wrapper's **cause**. Nothing is lost — but nothing is visible from the top of the trace either, because the top frames belong to the invoker. ## Reading the chain The diagnosis is mechanical once you know the shape: 1. Look at the failure's **cause**. If there is none, stop: this is a wiring defect, and the message names the member or the argument that did not fit. 2. If there is a cause, walk down. The invoker contributed exactly one layer; everything below it was produced by your code. 3. Read the **deepest** frames for the fault, but read the layers above it too — a target that deliberately wrapped a lower failure in a domain exception built that chain on purpose, and flattening straight to the root throws away the classification it was giving you. 4. Report the layer that is meaningful at your boundary, with the rest of the chain still attached. ## What breaks when you key on the wrapper - **A handler for the target's own exception type never fires.** The object in flight is the wrapper; the type you named is one level down. The code compiles, reads correctly and silently does nothing. - **Retry logic keyed on the wrapper type retries everything.** Every target failure arrives wearing the same type, so a permanent fault — bad input, a violated invariant — is retried with the same enthusiasm as a transient one. - **Error metrics collapse.** A dashboard counting exception types shows one bucket with every distinct fault in it, which is indistinguishable from a system that has one problem. - **A log line prints the invoker's message.** The wrapper's own text says the invocation failed, which is the one thing you already knew. - **A wiring bug is reported as an application bug.** "Could not build the thing" and "the thing failed" reach your alerting as the same event unless you split them on the presence of a cause. ## Handling rules that hold up - **Classify on the cause, never on the wrapper.** Every decision — retry, compensate, fail the startup, page someone — reads the cause chain. - **Unwrap exactly the invoker's layer** when re-raising across your own boundary. Unwrapping to the root is over-correction; it discards intermediate layers your own code built deliberately. - **Never flatten a chain into a message string.** The frames are the diagnosis, and text cannot be walked by code. - **Split the two families in your telemetry.** A container should report "this entry could not be built" and "this entry's method failed" as different events, because different people fix them. - **Make the machinery's messages specific.** When the call never happened, the message should name the member it looked for and the argument that did not fit; that message is the entire diagnosis for the most common case.
- Which failures from a dynamic call arrive with no cause attached, and why does that matter?The ones raised before the body ran: no member with that name and signature, a wrong argument count, a value that would not bind, access refused. They come from the machinery rather than from your logic, so an absent cause is a reliable signal that you are looking at a wiring or manifest defect and should stop reading application frames.
- You re-raise across your own module boundary. Do you unwrap to the root cause?No — unwrap the invoker's layer only. Layers your own code added were added deliberately and carry classification a caller may need. Strip the wrapper that exists purely because the call was dynamic, raise the meaningful failure, and leave the remaining chain attached so the frames survive.
- How should a container's telemetry treat the two families?As separate events. "This entry could not be built" is a deployment or manifest defect and usually blocks startup; "this entry's method failed" is an application defect that may be retried or compensated. Counting them in one bucket hides which of the two is happening, and they are fixed by different people.
A returned parcel carries one outer label reading "delivery failed". The note folded inside is the only place that says whether the address did not exist or the recipient refused it — and the two need different people to fix them.
saying these in an interview costs you the question
- Logs the wrapper's message and stops, never walking the cause chain.
- Writes a handler for the target's own exception type around a dynamic call.
- Treats "no matching member" and "the target threw" as the same failure.
- Retries on the wrapper type, so a permanent fault is retried forever.
- Flattens the cause chain into a message string and loses the frames.