The case-management tool's file-upload path takes a local file and posts it to the production API. The command-line invocation looks like merus-fetch /uploads/add file=@/path/to/document.pdf case_file_id=N. Internally, the code resolves the @/path prefix as a file-attach marker, reads the bytes off disk, builds a multipart form, and POSTs.

The defensive code that wraps this has been in for a while. If the file path doesn't exist, the tool exits with Error: File not found: /path. The check happens before any network call. The error is named at the layer that failed (the local filesystem) and points at the path the attorney needs to fix.

The probe this week was to ask what happens when the file exists but isn't readable — say the attorney downloaded the file with restrictive permissions, or the file is owned by another user, or it's on a network mount that's offline. The existence check passes. The next line, readFileSync(path), throws. The exception propagates up to the outermost try/catch around the whole request. That outer catch is labeled "API request failed" because for most error paths through the function, the API request IS what failed. For this one path, the error happens before any HTTP construction. The label was wrong.

The attorney looking at this error sees "API request failed" with an EACCES detail buried in the message. EACCES is "permission denied" — but it's permission denied on a local file open, not on a remote API authorization. The same error string could in principle mean either thing. The label leading the message tells the attorney which interpretation to apply. The label was sending them to the wrong system.

Where errors should be named

The principle is small: an error should be named at the layer that actually failed. Local filesystem read fails → "Cannot read file." DNS resolution fails → "Cannot reach host." HTTP returns 4xx → "API rejected the request." HTTP returns 5xx → "Server error." Each of these is a different system, and each sends the attorney to a different fix.

The wrong way to do this is to have a single outer try/catch that catches every kind of error and labels them all the same. That's what we had. It's also what most simple code does, because the outer try/catch is easier to write than a careful inner one. The cost shows up when something actually fails and the human reader has to guess which layer.

The fix this release is one inner try/catch around readFileSync with a "Cannot read file" message. The outer "API request failed" handler still exists for everything else. The local-fs error is now named at the local-fs layer; the remote-API error stays named at the remote-API layer. Two error sources, two error messages, no ambiguity.

What this doesn't catch

The same principle has more applications than just file uploads. Anywhere the system catches a generic exception and rebrands it, there's a chance the actual cause was in a layer the rebrand doesn't match. DNS lookup failures inside an API client get reported as "API failed." JSON parse failures on a malformed response get reported as "API failed." Stack overflow inside response handling gets reported as "API failed."

None of these are immediately broken in any visible way. The system stops, the attorney sees an error, the attorney tells someone the API is down. The someone investigates the API and finds it healthy. Hours later, the actual cause (their laptop's DNS resolver was misconfigured, or the file they were uploading was malformed in a way that broke the JSON parser before the request, or whatever) gets identified — but only because someone competent at investigating thought to look beyond the error message's label.

This release didn't audit every outer try/catch in the codebase. It fixed the one that was visible in the probe. The next probe might find another. The habit is to look at each "X failed" message and ask whether X actually failed, or whether something else inside X's region failed and got mislabeled. Each one of those is a small fix. Cumulatively, they make the system honest about which layer is asking for the attorney's attention.

The version on the install URL is 4.5.74. Small release. The work continues at the pace of one verified release at a time.