Skip to content

Commit 1f1bb5e

Browse files
authored
Make Error a persistent reference (#32)
A Napi::Error instance must hold a persistent reference to the JS error object so that it can escape from handle scopes when thrown as a C++ exception. Fixes: #31
1 parent 97bc4c1 commit 1f1bb5e

File tree

4 files changed

+178
-106
lines changed

4 files changed

+178
-106
lines changed

napi-inl.h

+49-10
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,8 @@ inline void Buffer<T>::EnsureInfo() const {
13811381
////////////////////////////////////////////////////////////////////////////////
13821382

13831383
inline Error Error::New(napi_env env) {
1384+
HandleScope scope(env);
1385+
13841386
napi_status status;
13851387
napi_value error = nullptr;
13861388
if (Napi::Env(env).IsExceptionPending()) {
@@ -1432,16 +1434,49 @@ inline Error Error::New(napi_env env, const std::string& message) {
14321434
return Error::New<Error>(env, message.c_str(), message.size(), napi_create_error);
14331435
}
14341436

1435-
inline Error::Error() : Object(), _message(nullptr) {
1437+
inline Error::Error() : ObjectReference(), _message(nullptr) {
14361438
}
14371439

1438-
inline Error::Error(napi_env env, napi_value value) : Object(env, value) {
1440+
inline Error::Error(napi_env env, napi_value value) : ObjectReference(env, nullptr) {
1441+
if (value != nullptr) {
1442+
napi_status status = napi_create_reference(env, value, 1, &_ref);
1443+
1444+
// Avoid infinite recursion in the failure case.
1445+
// Don't try to construct & throw another Error instance.
1446+
assert(status == napi_ok);
1447+
}
1448+
}
1449+
1450+
inline Error::Error(Error&& other) : ObjectReference(std::move(other)) {
1451+
}
1452+
1453+
inline Error& Error::operator =(Error&& other) {
1454+
static_cast<Reference<Object>*>(this)->operator=(std::move(other));
1455+
return *this;
1456+
}
1457+
1458+
inline Error::Error(const Error& other) : Error(other.Env(), other.Value()) {
1459+
}
1460+
1461+
inline Error& Error::operator =(Error& other) {
1462+
Reset();
1463+
1464+
_env = other.Env();
1465+
HandleScope scope(_env);
1466+
1467+
napi_value value = other.Value();
1468+
if (value != nullptr) {
1469+
napi_status status = napi_create_reference(_env, value, 1, &_ref);
1470+
if (status != napi_ok) throw Error::New(_env);
1471+
}
1472+
1473+
return *this;
14391474
}
14401475

14411476
inline const std::string& Error::Message() const NAPI_NOEXCEPT {
14421477
if (_message.size() == 0 && _env != nullptr) {
14431478
try {
1444-
_message = (*this)["message"].As<String>();
1479+
_message = Get("message").As<String>();
14451480
}
14461481
catch (...) {
14471482
// Catch all errors here, to include e.g. a std::bad_alloc from
@@ -1453,8 +1488,10 @@ inline const std::string& Error::Message() const NAPI_NOEXCEPT {
14531488
}
14541489

14551490
inline void Error::ThrowAsJavaScriptException() const {
1456-
if (_value != nullptr) {
1457-
napi_throw(_env, _value);
1491+
HandleScope scope(_env);
1492+
if (!IsEmpty()) {
1493+
napi_status status = napi_throw(_env, Value());
1494+
if (status != napi_ok) throw Error::New(_env);
14581495
}
14591496
}
14601497

@@ -1532,8 +1569,8 @@ inline Reference<T>::Reference() : _env(nullptr), _ref(nullptr), _suppressDestru
15321569
}
15331570

15341571
template <typename T>
1535-
inline Reference<T>::Reference(napi_env env, napi_ref persistent)
1536-
: _env(env), _ref(persistent) {
1572+
inline Reference<T>::Reference(napi_env env, napi_ref ref)
1573+
: _env(env), _ref(ref) {
15371574
}
15381575

15391576
template <typename T>
@@ -1557,6 +1594,7 @@ inline Reference<T>::Reference(Reference<T>&& other) {
15571594

15581595
template <typename T>
15591596
inline Reference<T>& Reference<T>::operator =(Reference<T>&& other) {
1597+
Reset();
15601598
_env = other._env;
15611599
_ref = other._ref;
15621600
other._env = nullptr;
@@ -2626,7 +2664,7 @@ inline void AsyncWorker::WorkComplete() {
26262664
OnOK();
26272665
}
26282666
else {
2629-
OnError(_error.Value());
2667+
OnError(_error);
26302668
}
26312669
}
26322670

@@ -2639,11 +2677,12 @@ inline void AsyncWorker::OnOK() {
26392677
}
26402678

26412679
inline void AsyncWorker::OnError(Error e) {
2642-
_callback.MakeCallback(Env().Undefined(), std::vector<napi_value>({ e }));
2680+
HandleScope scope(Env());
2681+
_callback.MakeCallback(Env().Undefined(), std::vector<napi_value>({ e.Value() }));
26432682
}
26442683

26452684
inline void AsyncWorker::SetError(Error error) {
2646-
_error.Reset(error, 1);
2685+
_error = error;
26472686
}
26482687

26492688
inline void AsyncWorker::OnExecute(napi_env env, void* this_pointer) {

napi.h

+99-94
Original file line numberDiff line numberDiff line change
@@ -443,99 +443,6 @@ namespace Napi {
443443
void EnsureInfo() const;
444444
};
445445

446-
/*
447-
* The NAPI Error class wraps a JavaScript Error object in a way that enables it
448-
* to traverse a C++ stack and be thrown and caught as a C++ exception.
449-
*
450-
* If a NAPI API call fails without executing any JavaScript code (for example due
451-
* to an invalid argument), then the NAPI wrapper automatically converts and throws
452-
* the error as a C++ exception of type Napi::Error.
453-
*
454-
* If a JavaScript function called by C++ code via NAPI throws a JavaScript exception,
455-
* then the NAPI wrapper automatically converts and throws it as a C++ exception of type
456-
* Napi::Error.
457-
*
458-
* If a C++ exception of type Napi::Error escapes from a NAPI C++ callback, then
459-
* the NAPI wrapper automatically converts and throws it as a JavaScript exception.
460-
*
461-
* Catching a C++ exception of type Napi::Error also clears the JavaScript exception.
462-
* Of course it may be then re-thrown, which restores the JavaScript exception.
463-
*
464-
* Example 1 - Throwing an exception:
465-
*
466-
* Napi::Env env = ...
467-
* throw env.NewError("Example exception");
468-
* // Following C++ statements will not be executed.
469-
* // The exception will bubble up as a C++ exception of type Napi::Error,
470-
* // until it is either caught while still in C++, or else automatically
471-
* // re-thrown as a JavaScript exception when the callback returns to JavaScript.
472-
*
473-
* Example 2 - Ignoring a NAPI exception:
474-
*
475-
* Napi::Function jsFunctionThatThrows = someObj.AsFunction();
476-
* jsFunctionThatThrows({ arg1, arg2 });
477-
* // Following C++ statements will not be executed.
478-
* // The exception will bubble up as a C++ exception of type Napi::Error,
479-
* // until it is either caught while still in C++, or else automatically
480-
* // re-thrown as a JavaScript exception when the callback returns to JavaScript.
481-
*
482-
* Example 3 - Handling a NAPI exception:
483-
*
484-
* Napi::Function jsFunctionThatThrows = someObj.AsFunction();
485-
* try {
486-
* jsFunctionThatThrows({ arg1, arg2 });
487-
* }
488-
* catch (const Napi::Error& e) {
489-
* cerr << "Caught JavaScript exception: " + e.what();
490-
* // Since the exception was caught here, it will not be re-thrown as
491-
* // a JavaScript exception.
492-
* }
493-
*/
494-
class Error : public Object, public std::exception {
495-
public:
496-
static Error New(napi_env env);
497-
static Error New(napi_env env, const char* message);
498-
static Error New(napi_env env, const std::string& message);
499-
500-
Error();
501-
Error(napi_env env, napi_value value);
502-
503-
const std::string& Message() const NAPI_NOEXCEPT;
504-
void ThrowAsJavaScriptException() const;
505-
506-
const char* what() const NAPI_NOEXCEPT override;
507-
508-
protected:
509-
typedef napi_status (*create_error_fn)(napi_env envb, napi_value msg, napi_value* result);
510-
511-
template <typename TError>
512-
static TError New(napi_env env,
513-
const char* message,
514-
size_t length,
515-
create_error_fn create_error);
516-
517-
private:
518-
mutable std::string _message;
519-
};
520-
521-
class TypeError : public Error {
522-
public:
523-
static TypeError New(napi_env env, const char* message);
524-
static TypeError New(napi_env env, const std::string& message);
525-
526-
TypeError();
527-
TypeError(napi_env env, napi_value value);
528-
};
529-
530-
class RangeError : public Error {
531-
public:
532-
static RangeError New(napi_env env, const char* message);
533-
static RangeError New(napi_env env, const std::string& message);
534-
535-
RangeError();
536-
RangeError(napi_env env, napi_value value);
537-
};
538-
539446
/*
540447
* Holds a counted reference to a value; initially a weak reference unless otherwise specified.
541448
* May be changed to/from a strong reference by adjusting the refcount. The referenced value
@@ -660,6 +567,104 @@ namespace Napi {
660567
ObjectReference Persistent(Object value);
661568
FunctionReference Persistent(Function value);
662569

570+
/*
571+
* The NAPI Error class wraps a JavaScript Error object in a way that enables it
572+
* to traverse a C++ stack and be thrown and caught as a C++ exception.
573+
*
574+
* If a NAPI API call fails without executing any JavaScript code (for example due
575+
* to an invalid argument), then the NAPI wrapper automatically converts and throws
576+
* the error as a C++ exception of type Napi::Error.
577+
*
578+
* If a JavaScript function called by C++ code via NAPI throws a JavaScript exception,
579+
* then the NAPI wrapper automatically converts and throws it as a C++ exception of type
580+
* Napi::Error.
581+
*
582+
* If a C++ exception of type Napi::Error escapes from a NAPI C++ callback, then
583+
* the NAPI wrapper automatically converts and throws it as a JavaScript exception.
584+
*
585+
* Catching a C++ exception of type Napi::Error also clears the JavaScript exception.
586+
* Of course it may be then re-thrown, which restores the JavaScript exception.
587+
*
588+
* Example 1 - Throwing a N-API exception:
589+
*
590+
* Napi::Env env = ...
591+
* throw Napi::Error::New(env, "Example exception");
592+
* // Following C++ statements will not be executed.
593+
* // The exception will bubble up as a C++ exception of type Napi::Error,
594+
* // until it is either caught while still in C++, or else automatically
595+
* // re-thrown as a JavaScript exception when the callback returns to JavaScript.
596+
*
597+
* Example 2 - Ignoring a N-API exception:
598+
*
599+
* Napi::Function jsFunctionThatThrows = someObj.As<Napi::Function>();
600+
* jsFunctionThatThrows({ arg1, arg2 });
601+
* // Following C++ statements will not be executed.
602+
* // The exception will bubble up as a C++ exception of type Napi::Error,
603+
* // until it is either caught while still in C++, or else automatically
604+
* // re-thrown as a JavaScript exception when the callback returns to JavaScript.
605+
*
606+
* Example 3 - Handling a N-API exception:
607+
*
608+
* Napi::Function jsFunctionThatThrows = someObj.As<Napi::Function>();
609+
* try {
610+
* jsFunctionThatThrows({ arg1, arg2 });
611+
* } catch (const Napi::Error& e) {
612+
* cerr << "Caught JavaScript exception: " + e.Message();
613+
* // Since the exception was caught here, it will not be re-thrown as
614+
* // a JavaScript exception.
615+
* }
616+
*/
617+
class Error : public ObjectReference, public std::exception {
618+
public:
619+
static Error New(napi_env env);
620+
static Error New(napi_env env, const char* message);
621+
static Error New(napi_env env, const std::string& message);
622+
623+
Error();
624+
Error(napi_env env, napi_value value);
625+
626+
// An error can be moved or copied.
627+
Error(Error&& other);
628+
Error& operator =(Error&& other);
629+
Error(const Error&);
630+
Error& operator =(Error&);
631+
632+
const std::string& Message() const NAPI_NOEXCEPT;
633+
void ThrowAsJavaScriptException() const;
634+
635+
const char* what() const NAPI_NOEXCEPT override;
636+
637+
protected:
638+
typedef napi_status (*create_error_fn)(napi_env envb, napi_value msg, napi_value* result);
639+
640+
template <typename TError>
641+
static TError New(napi_env env,
642+
const char* message,
643+
size_t length,
644+
create_error_fn create_error);
645+
646+
private:
647+
mutable std::string _message;
648+
};
649+
650+
class TypeError : public Error {
651+
public:
652+
static TypeError New(napi_env env, const char* message);
653+
static TypeError New(napi_env env, const std::string& message);
654+
655+
TypeError();
656+
TypeError(napi_env env, napi_value value);
657+
};
658+
659+
class RangeError : public Error {
660+
public:
661+
static RangeError New(napi_env env, const char* message);
662+
static RangeError New(napi_env env, const std::string& message);
663+
664+
RangeError();
665+
RangeError(napi_env env, napi_value value);
666+
};
667+
663668
class CallbackInfo {
664669
public:
665670
CallbackInfo(napi_env env, napi_callback_info info);
@@ -985,7 +990,7 @@ namespace Napi {
985990

986991
napi_env _env;
987992
napi_async_work _work;
988-
Reference<Error> _error;
993+
Error _error;
989994
};
990995

991996
} // namespace Napi

test/error.cc

+22-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Value CatchError(const CallbackInfo& info) {
2424
try {
2525
thrower({});
2626
} catch (const Error& e) {
27-
return e;
27+
return e.Value();
2828
}
2929
return info.Env().Null();
3030
}
@@ -50,11 +50,28 @@ void CatchAndRethrowError(const CallbackInfo& info) {
5050
try {
5151
thrower({});
5252
} catch (Error& e) {
53-
e["caught"] = Boolean::New(info.Env(), true);
53+
e.Set("caught", Boolean::New(info.Env(), true));
5454
throw e;
5555
}
5656
}
5757

58+
void ThrowErrorThatEscapesScope(const CallbackInfo& info) {
59+
HandleScope scope(info.Env());
60+
61+
std::string message = info[0].As<String>().Utf8Value();
62+
throw Error::New(info.Env(), message);
63+
}
64+
65+
void CatchAndRethrowErrorThatEscapesScope(const CallbackInfo& info) {
66+
HandleScope scope(info.Env());
67+
try {
68+
ThrowErrorThatEscapesScope(info);
69+
} catch (Error& e) {
70+
e.Set("caught", Boolean::New(info.Env(), true));
71+
throw e;
72+
}
73+
}
74+
5875
} // end anonymous namespace
5976

6077
Object InitError(Env env) {
@@ -66,5 +83,8 @@ Object InitError(Env env) {
6683
exports["catchErrorMessage"] = Function::New(env, CatchErrorMessage);
6784
exports["doNotCatch"] = Function::New(env, DoNotCatch);
6885
exports["catchAndRethrowError"] = Function::New(env, CatchAndRethrowError);
86+
exports["throwErrorThatEscapesScope"] = Function::New(env, ThrowErrorThatEscapesScope);
87+
exports["catchAndRethrowErrorThatEscapesScope"] =
88+
Function::New(env, CatchAndRethrowErrorThatEscapesScope);
6989
return exports;
7090
}

test/error.js

+8
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,11 @@ assert.strictEqual(err.message, 'test');
4141
const msg = binding.error.catchErrorMessage(
4242
() => { throw new TypeError('test'); });
4343
assert.strictEqual(msg, 'test');
44+
45+
assert.throws(() => binding.error.throwErrorThatEscapesScope('test'), err => {
46+
return err instanceof Error && err.message === 'test';
47+
});
48+
49+
assert.throws(() => binding.error.catchAndRethrowErrorThatEscapesScope('test'), err => {
50+
return err instanceof Error && err.message === 'test' && err.caught;
51+
});

0 commit comments

Comments
 (0)