openlang

Wrapping exceptions without losing the cause

new ImportFailure("Import failed") is all that reaches the diagnostic log once the import tool catches a low-level failure and replaces it. The caller has no business seeing local file details. The person investigating the failure still needs to know what went wrong underneath.

Keep the domain exception, and in its constructor pass the original failure to Throwable as the cause. Java gives you that causal link without turning the public exception category into the lower-level one.

final class ImportFailure extends Exception {
    ImportFailure(String message, Throwable cause) {
        super(message, cause);
    }
}

try {
    readInput(path);
} catch (IOException e) {
    throw new ImportFailure("Import failed", e);
}

That is the whole change on the throwing side. I'd also delete the constructor that drops the cause, since leaving it there invites the next catch block to use it.

The public response then uses the domain category and a message someone chose on purpose. It should never borrow its wording from getCause().getMessage(), because the cause may hold the very local details we meant to keep out. Internal diagnostics need deliberate handling as well. "Print every exception value" is not a policy.

Richer diagnostics come with access and redaction rules, and that burden is real. Every place the chained exception travels to has to be checked for what it prints, and some of those places print more than their authors expect. Dropping the cause avoids all of that and leaves you investigating from two words. I'll take the redaction work, but I want the list of destinations written down before the first cause gets attached.

后续讨论 7

顺着彼此的依据读下去
  1. 固定链接

    Two checks, one after the other. First, getCause() on the wrapper should return the original failure. Once that holds, build the public response from the domain category without ever reading the cause message.

    Then put data/input.csv into the original message and assert that the response leaves it out. That covers the response. The log entry is another destination with its own fields, and this test tells you nothing about which of those are safe to show, so the logging path gets its own review.

  2. 固定链接

    Can you post just the wrapper construction and the logging call, for one failure? Attaching a cause puts nothing in the log on its own, so the logging line is where I'd look first. Use a dummy path, then read the log entry and the public response one at a time. That's small enough to spot where the information goes missing.

  3. 固定链接

    Not much of a Java writer myself, but Cocoa settled this long ago with NSUnderlyingErrorKey. The domain-level NSError you hand back carries the lower-level error in its userInfo under that key, localizedDescription is written for the person reading the alert, and the underlying error stays available to whoever logs it deliberately.

    The same caution applies, though. Logging the whole userInfo dictionary is the Objective-C version of printing every exception value, and it will happily include the file path you meant to keep private.

  4. 固定链接

    First destination to check is the stack trace itself. It prints the cause with no help from anyone.

    try {
        Files.readString(Path.of("data/input.csv"));
    } catch (IOException e) {
        new ImportFailure("Import failed", e).printStackTrace();
    }

    The output has a Caused by: java.nio.file.NoSuchFileException: data/input.csv line. The path is the exception message, and the trace format includes every cause's message. Any logger that formats the throwable does the same. So redaction means not handing the wrapper to the log as it is.

  5. 固定链接

    A question about the restricted storage side, because I can't tell how you're walking the chain. Does the writer follow getSuppressed() as well as getCause()? I tried a try-with-resources version where both read() and close() fail. The close failure ends up attached to the IOException as a suppressed exception, and printStackTrace prints it under a Suppressed: line with its own message, which in my test held the path again. A writer that only loops on getCause() would skip it, so the restricted copy would lack exactly the close failure someone might want later. Does yours walk both?

同一个问题的其他角度

  1. `String::from_utf8` gives the bytes back when it fails

    A consuming API has to decide what happens to the value when it fails. String::from_utf8 takes the Vec<u8> by value, and on f…

    回复 0最近活动 rust
  2. Where does the cursor stay after an incomplete record?

    Result tells the caller of a streaming parser whether a record came out. It says nothing about whether the parser moved a cur…

    回复 9最近活动 rustocamlzig
  3. `result.map_error` on every line of a use chain

    use with result.try is my favourite bit of Gleam and I'm starting to resent it a little. Every step calls a different module,…

    回复 1最近活动 gleam