@@ -556,6 +556,10 @@ Napi::Value DatabaseSync::Prepare(const Napi::CallbackInfo &info) {
556556
557557 std::string sql = info[0 ].As <Napi::String>().Utf8Value ();
558558
559+ // Clear any stale deferred exception from a previous operation
560+ ClearDeferredAuthorizerException ();
561+ SetIgnoreNextSQLiteError (false );
562+
559563 try {
560564 // Create new StatementSync instance using addon data constructor
561565 AddonData *addon_data = GetAddonData (env);
@@ -573,6 +577,25 @@ Napi::Value DatabaseSync::Prepare(const Napi::CallbackInfo &info) {
573577
574578 return stmt_obj;
575579 } catch (const std::exception &e) {
580+ // Handle deferred authorizer exceptions:
581+ //
582+ // When an authorizer callback throws a JavaScript exception, we use a
583+ // "marker" exception pattern to safely propagate the error:
584+ //
585+ // 1. On Windows (MSVC), std::exception::what() can sometimes return an
586+ // empty string, causing message loss.
587+ //
588+ // 2. By storing the message in the DatabaseSync instance, we can retrieve
589+ // it here and throw a proper JavaScript exception with the original text.
590+ //
591+ // See also: StatementSync::InitStatement for the other half of this pattern.
592+ if (HasDeferredAuthorizerException ()) {
593+ std::string deferred_msg = GetDeferredAuthorizerException ();
594+ ClearDeferredAuthorizerException ();
595+ SetIgnoreNextSQLiteError (false );
596+ Napi::Error::New (env, deferred_msg).ThrowAsJavaScriptException ();
597+ return env.Undefined ();
598+ }
576599 node::THROW_ERR_SQLITE_ERROR (env, e.what ());
577600 return env.Undefined ();
578601 }
@@ -597,6 +620,10 @@ Napi::Value DatabaseSync::Exec(const Napi::CallbackInfo &info) {
597620
598621 std::string sql = info[0 ].As <Napi::String>().Utf8Value ();
599622
623+ // Clear any stale deferred exception from a previous operation
624+ ClearDeferredAuthorizerException ();
625+ SetIgnoreNextSQLiteError (false );
626+
600627 char *error_msg = nullptr ;
601628 int result =
602629 sqlite3_exec (connection (), sql.c_str (), nullptr , nullptr , &error_msg);
@@ -1459,12 +1486,23 @@ void StatementSync::InitStatement(DatabaseSync *database,
14591486 &statement_, &tail);
14601487
14611488 if (result != SQLITE_OK) {
1462- // Check for deferred authorizer exception first
1489+ // Handle deferred authorizer exceptions:
1490+ //
1491+ // When an authorizer callback throws a JavaScript exception, we use a
1492+ // "marker" exception pattern to safely propagate the error:
1493+ //
1494+ // 1. On Windows (MSVC), std::exception::what() can sometimes return an
1495+ // empty string, causing message loss.
1496+ //
1497+ // 2. By storing the message in the DatabaseSync instance, the caller can
1498+ // retrieve it and throw a proper JavaScript exception with the original text.
1499+ //
1500+ // 3. This matches Node.js's behavior where JavaScript exceptions from
1501+ // authorizer callbacks propagate correctly to the caller.
14631502 if (database->HasDeferredAuthorizerException ()) {
1464- std::string deferred_msg = database->GetDeferredAuthorizerException ();
1465- database->ClearDeferredAuthorizerException ();
1466- database->SetIgnoreNextSQLiteError (false );
1467- throw std::runtime_error (deferred_msg);
1503+ // Throw a marker exception - the actual message is stored in the database
1504+ // object and will be retrieved by the caller.
1505+ throw std::runtime_error (" " );
14681506 }
14691507 std::string error = sqlite3_errmsg (database->connection ());
14701508 throw std::runtime_error (" Failed to prepare statement: " + error);
@@ -2929,7 +2967,7 @@ int DatabaseSync::AuthorizerCallback(void *user_data, int action_code,
29292967 if (int_result != SQLITE_OK && int_result != SQLITE_DENY &&
29302968 int_result != SQLITE_IGNORE) {
29312969 db->SetDeferredAuthorizerException (
2932- " Authorizer callback returned a invalid authorization code" );
2970+ " Authorizer callback returned an invalid authorization code" );
29332971 db->SetIgnoreNextSQLiteError (true );
29342972 return SQLITE_DENY;
29352973 }
0 commit comments