src: report libuv error when openAsBlob cannot stat
Project: nodejs/node | Language: C++/JavaScript | Concepts: error-handling, api-design | Difficulty: mid
Link: https://github.com/nodejs/node/pull/65517
The problem
When calling fs.openAsBlob() on a file path that does not exist, Node.js previously threw a generic TypeError [ERR_INVALID_ARG_VALUE]: Unable to open file as blob. This error did not indicate the real cause (such as the file being missing), nor did it provide useful details like errno, syscall, or path. This made it hard for users to distinguish between different failure reasons.
Why it's non-trivial
A naive fix might simply change the error message, but would still not pass through the actual error code or context from the underlying filesystem operation. Proper error handling should surface the real system error (e.g., ENOENT for missing files) and provide context, so that calling code can distinguish between different failure modes and handle them appropriately.
The approach
1. Threading the error code out of FdEntry::Create
Previously, FdEntry::Create would just return nullptr if uv_fs_stat failed, dropping the actual error code:
- static std::unique_ptr<FdEntry> Create(Environment* env, Local<Value> path) {
+ static std::unique_ptr<FdEntry> Create(Environment* env,
+ Local<Value> path,
+ int* status) {
...
- if (uv_fs_stat(nullptr, &req, buf->out(), nullptr) < 0) return nullptr;
+ int err = uv_fs_stat(nullptr, &req, buf->out(), nullptr);
+ if (err < 0) {
+ if (status != nullptr) *status = err;
+ return nullptr;
+ }
What this does:
- Adds a
statusout-parameter to capture the error code fromuv_fs_stat. - If the stat fails, the error code is stored in
statusbefore returningnullptr.
2. Propagating the error code up through DataQueue::CreateFdEntry
-std::unique_ptr<DataQueue::Entry> DataQueue::CreateFdEntry(Environment* env,
- Local<Value> path) {
- return FdEntry::Create(env, path);
+std::unique_ptr<DataQueue::Entry> DataQueue::CreateFdEntry(Environment* env,
+ Local<Value> path,
+ int* status) {
+ return FdEntry::Create(env, path, status);
}
What this does:
- Updates the
CreateFdEntrymethod to accept and pass through the error status.
3. Throwing a detailed UVException in BlobFromFilePath
- auto entry = DataQueue::CreateFdEntry(env, args[0]);
+ int status = 0;
+ auto entry = DataQueue::CreateFdEntry(env, args[0], &status);
if (entry == nullptr) {
- return THROW_ERR_INVALID_ARG_VALUE(env, "Unable to open file as blob");
+ // The file could not be stat'd. Report the libuv error so callers can tell
+ // ENOENT apart from any other reason the path could not be used.
+ return env->ThrowUVException(status, "stat", nullptr, *path);
}
What this does:
- Calls
CreateFdEntrywith a pointer tostatus, capturing the error. - If the entry is
nullptr, throws aUVExceptionwith the actual error code, syscall name, and path, matching the behavior of other Node.js filesystem APIs.
4. Adding a test to assert the new error behavior
+(async () => {
+ // A path that cannot be stat'd reports the underlying libuv error rather
+ // than a generic argument error, so ENOENT can be told apart from any other
+ // reason the file could not be used.
+ // Refs: https://github.com/nodejs/node/issues/65514
+ const missing = tmpdir.resolve('does-not-exist.txt');
+ await assert.rejects(async () => openAsBlob(missing), {
+ code: 'ENOENT',
+ syscall: 'stat',
+ path: missing,
+ });
+})().then(common.mustCall());
What this does:
- Verifies that trying to open a non-existent file as a blob now throws an error with the correct code, syscall, and path, not a generic argument error.
From the review thread
No notable review discussion on this PR.
Takeaways
- Always propagate and report the underlying system error when wrapping lower-level APIs, so that callers can distinguish between different failure causes.
- Avoid masking system errors with generic messages—surface context like error code, syscall, and path to aid debugging and correct error handling.
- When changing error behavior, update or add tests to assert the new, more informative error output.