Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logic to handle graceful error handling when primitive errors are thrown by JS functions #1075

Merged
merged 7 commits into from
Nov 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/error_handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ If C++ exceptions are enabled (for more info see: [Setup](setup.md)), then the
`Napi::Error` class extends `std::exception` and enables integrated
error-handling for C++ exceptions and JavaScript exceptions.

Note, that due to limitations of the Node-API, if one attempts to cast the error object wrapping a primitive inside a C++ addon, the wrapped object
will be received instead. (With property `4bda9e7e-4913-4dbc-95de-891cbf66598e-errorVal` containing the primitive value thrown)

gabrielschulhof marked this conversation as resolved.
Show resolved Hide resolved
The following sections explain the approach for each case:

- [Handling Errors With C++ Exceptions](#exceptions)
Expand Down
69 changes: 68 additions & 1 deletion napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2593,14 +2593,80 @@ inline Error::Error() : ObjectReference() {

inline Error::Error(napi_env env, napi_value value) : ObjectReference(env, nullptr) {
if (value != nullptr) {
// Attempting to create a reference on the error object.
// If it's not a Object/Function/Symbol, this call will return an error
// status.
napi_status status = napi_create_reference(env, value, 1, &_ref);

if (status != napi_ok) {
napi_value wrappedErrorObj;

// Create an error object
status = napi_create_object(env, &wrappedErrorObj);
NAPI_FATAL_IF_FAILED(status, "Error::Error", "napi_create_object");

// property flag that we attach to show the error object is wrapped
napi_property_descriptor wrapObjFlag = {
ERROR_WRAP_VALUE, // Unique GUID identifier since Symbol isn't a
// viable option
nullptr,
nullptr,
nullptr,
nullptr,
Value::From(env, value),
napi_enumerable,
nullptr};

status = napi_define_properties(env, wrappedErrorObj, 1, &wrapObjFlag);
NAPI_FATAL_IF_FAILED(status, "Error::Error", "napi_define_properties");

// Create a reference on the newly wrapped object
status = napi_create_reference(env, wrappedErrorObj, 1, &_ref);
}

// Avoid infinite recursion in the failure case.
// Don't try to construct & throw another Error instance.
NAPI_FATAL_IF_FAILED(status, "Error::Error", "napi_create_reference");
}
}

inline Object Error::Value() const {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might make sense for this to return Value instead of Object (Values can be objects), the main concern is that would potentially be breaking.

The reason is that we wrapping so that we can have a value versus just objects. If its possible we might want two methods one which returns a Value and one which returns an Object in order to avoid breaking existing code.

Copy link
Member Author

@JckXia JckXia Sep 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried setting the return type to Value and it seems to pass all the test suites. Although I am not too sure if that breaks any conventions. (i.e, In the base Reference class, Reference<T>::Value() will return an instance of type T, and Error is of type Object since it inherits ObjectReference, which it self inherits from Reference<Object>. Therefore people might expect Error::Value() to return an Object also)

For the second part then, can I declare two public methods Napi::Value GetUnWrappedValue() and Object Value() ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If is possible to have 2 methods both called Value() which return different types. If so that might make sense assuming the compiler will select the most appropriate one based on how you use the return value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I am not too sure if it's possible to overload base on return value in C++ (without using templates) according to this post (https://stackoverflow.com/questions/9568852/overloading-by-return-type).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussing in Node-API team meeting I think this is ok as is.

if (_ref == nullptr) {
return Object(_env, nullptr);
}
gabrielschulhof marked this conversation as resolved.
Show resolved Hide resolved

napi_value refValue;
napi_status status = napi_get_reference_value(_env, _ref, &refValue);
NAPI_THROW_IF_FAILED(_env, status, Object());

napi_valuetype type;
status = napi_typeof(_env, refValue, &type);
NAPI_THROW_IF_FAILED(_env, status, Object());

// If refValue isn't a symbol, then we proceed to whether the refValue has the
// wrapped error flag
if (type != napi_symbol) {
// We are checking if the object is wrapped
bool isWrappedObject = false;

status = napi_has_property(
_env, refValue, String::From(_env, ERROR_WRAP_VALUE), &isWrappedObject);

// Don't care about status
if (isWrappedObject) {
napi_value unwrappedValue;
status = napi_get_property(_env,
refValue,
String::From(_env, ERROR_WRAP_VALUE),
&unwrappedValue);
NAPI_THROW_IF_FAILED(_env, status, Object());

return Object(_env, unwrappedValue);
}
}

return Object(_env, refValue);
}

inline Error::Error(Error&& other) : ObjectReference(std::move(other)) {
}

Expand Down Expand Up @@ -2651,6 +2717,7 @@ inline const std::string& Error::Message() const NAPI_NOEXCEPT {
return _message;
}

// we created an object on the &_ref
inline void Error::ThrowAsJavaScriptException() const {
HandleScope scope(_env);
if (!IsEmpty()) {
Expand Down
6 changes: 5 additions & 1 deletion napi.h
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,8 @@ namespace Napi {
const std::string& Message() const NAPI_NOEXCEPT;
void ThrowAsJavaScriptException() const;

Object Value() const;

#ifdef NAPI_CPP_EXCEPTIONS
const char* what() const NAPI_NOEXCEPT override;
#endif // NAPI_CPP_EXCEPTIONS
Expand All @@ -1718,7 +1720,9 @@ namespace Napi {
/// !endcond

private:
mutable std::string _message;
const char* ERROR_WRAP_VALUE =
"4bda9e7e-4913-4dbc-95de-891cbf66598e-errorVal";
mutable std::string _message;
};

class TypeError : public Error {
Expand Down
2 changes: 2 additions & 0 deletions test/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Object InitDate(Env env);
Object InitDataView(Env env);
Object InitDataViewReadWrite(Env env);
Object InitEnvCleanup(Env env);
Object InitErrorHandlingPrim(Env env);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these names consistent with the file names being used? This will help @deepakrkris with the unit test filtering.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deepakrkris can you double-check this please?

Object InitError(Env env);
Object InitExternal(Env env);
Object InitFunction(Env env);
Expand Down Expand Up @@ -113,6 +114,7 @@ Object Init(Env env, Object exports) {
exports.Set("env_cleanup", InitEnvCleanup(env));
#endif
exports.Set("error", InitError(env));
exports.Set("errorHandlingPrim", InitErrorHandlingPrim(env));
exports.Set("external", InitExternal(env));
exports.Set("function", InitFunction(env));
exports.Set("functionreference", InitFunctionReference(env));
Expand Down
1 change: 1 addition & 0 deletions test/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
'dataview/dataview_read_write.cc',
'env_cleanup.cc',
'error.cc',
'error_handling_for_primitives.cc',
'external.cc',
'function.cc',
'function_reference.cc',
Expand Down
13 changes: 13 additions & 0 deletions test/error_handling_for_primitives.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include <napi.h>

namespace {
void Test(const Napi::CallbackInfo& info) {
info[0].As<Napi::Function>().Call({});
}

} // namespace
Napi::Object InitErrorHandlingPrim(Napi::Env env) {
Napi::Object exports = Napi::Object::New(env);
exports.Set("errorHandlingPrim", Napi::Function::New<Test>(env));
return exports;
}
29 changes: 29 additions & 0 deletions test/error_handling_for_primitives.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

const assert = require('assert');

module.exports = require('./common').runTest((binding) => {
test(binding.errorHandlingPrim);
});

function canThrow (binding, errorMessage, errorType) {
try {
binding.errorHandlingPrim(() => {
throw errorMessage;
});
} catch (e) {
// eslint-disable-next-line valid-typeof
assert(typeof e === errorType);
assert(e === errorMessage);
}
}

function test (binding) {
canThrow(binding, '404 server not found!', 'string');
canThrow(binding, 42, 'number');
canThrow(binding, Symbol.for('newSym'), 'symbol');
canThrow(binding, false, 'boolean');
canThrow(binding, BigInt(123), 'bigint');
canThrow(binding, () => { console.log('Logger shutdown incorrectly'); }, 'function');
canThrow(binding, { status: 403, errorMsg: 'Not authenticated' }, 'object');
}