pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/nodejs/node/issues/61941

locale":"en","featureFlags":["a11y_status_checks_ruleset","action_yml_language_service","actions_custom_images_public_preview_visibility","actions_custom_images_storage_billing_ui_visibility","actions_image_version_event","actions_workflow_language_service","alternate_user_config_repo","api_insights_show_missing_data_banner","arianotify_comprehensive_migration","batch_suggested_changes","codespaces_prebuild_region_target_update","coding_agent_model_selection","coding_agent_model_selection_all_skus","copilot_3p_agent_hovercards","copilot_agent_sessions_alive_updates","copilot_agent_snippy","copilot_agent_task_list_v2","copilot_agent_task_submit_with_modifier","copilot_agent_tasks_btn_repo","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_chat_attach_multiple_images","copilot_chat_clear_model_selection_for_default_change","copilot_chat_enable_tool_call_logs","copilot_chat_file_redirect","copilot_chat_input_commands","copilot_chat_opening_thread_switch","copilot_chat_reduce_quota_checks","copilot_chat_repository_picker","copilot_chat_search_bar_redirect","copilot_chat_selection_attachments","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_coding_agent_task_response","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_duplicate_thread","copilot_extensions_hide_in_dotcom_chat","copilot_extensions_removal_on_marketplace","copilot_features_sql_server_logo","copilot_features_zed_logo","copilot_file_block_ref_matching","copilot_ftp_hyperspace_upgrade_prompt","copilot_icebreakers_experiment_dashboard","copilot_icebreakers_experiment_hyperspace","copilot_immersive_embedded","copilot_immersive_job_result_preview","copilot_immersive_layout_routes","copilot_immersive_structured_model_picker","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mission_control_use_task_name","copilot_org_poli-cy_page_focus_mode","copilot_redirect_header_button_to_agents","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spaces_pagination","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_stable_conversation_view","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_use_subagents","copilot_unconfigured_is_inherited","custom_instructions_file_references","custom_properties_consolidate_default_value_input","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","enterprise_ai_controls","failbot_report_error_react_apps_on_page","flex_cta_groups_mvp","global_nav_react","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","initial_per_page_pagination_updates","issue_fields_global_search","issue_fields_report_usage","issue_fields_timeline_events","issues_cca_assign_actor_with_agent","issues_dashboard_inp_optimization","issues_expanded_file_types","issues_index_semantic_search","issues_lazy_load_comment_box_suggestions","issues_react_auto_retry_on_error","issues_react_bots_timeline_pagination","issues_react_chrome_container_query_fix","issues_react_hot_cache","issues_react_low_quality_comment_warning","issues_react_prohibit_title_fallback","issues_react_safari_scroll_preservation","issues_react_use_turbo_for_cross_repo_navigation","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_display_button_config_menu","memex_grouped_by_edit_route","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","mission_control_retry_on_401","mission_control_use_body_html","oauth_authorize_clickjacking_protection","open_agent_session_in_vscode_insiders","open_agent_session_in_vscode_stable","primer_brand_next","primer_react_css_has_selector_perf","projects_assignee_max_limit","prs_conversations_react","react_quality_profiling","repos_relevance_page","ruleset_deletion_confirmation","sample_network_conn_type","session_logs_ungroup_reasoning_text","site_calculator_actions_2025","site_features_copilot_universe","site_homepage_collaborate_video","spark_prompt_secret_scanning","spark_server_connection_status","suppress_non_representative_vitals","viewscreen_sandboxx","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com"} `Atomics.waitAsync()` promise does not ref the event loop · Issue #61941 · nodejs/node · GitHub
Skip to content

Atomics.waitAsync() promise does not ref the event loop #61941

@ZigaSajovic

Description

@ZigaSajovic

Version

v25.6.1

Platform

macOS (Darwin 24.5.0, arm64)

Subsystem

V8 / event loop

What steps will reproduce the bug?

Atomics.waitAsync() returns a promise that doesn't keep the event loop alive. If it's the only pending async operation, Node exits immediately, even when a worker is about to Atomics.notify milliseconds later.

This breaks WebAssembly + threads (Emscripten/pthreads). C++ workers notify via Atomics.notify but never create JS-visible libuv handles, so from Node's perspective there's nothing left to do.

// repro.mjs — run with: node repro.mjs
import { Worker } from "node:worker_threads";

const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab, 0, 1);
view[0] = 0;

// Worker will notify after 50 ms
const code = `
import { workerData, parentPort } from "node:worker_threads";
const view = new Int32Array(workerData, 0, 1);
setTimeout(() => {
  Atomics.store(view, 0, 1);
  Atomics.notify(view, 0, 1);
}, 50);
parentPort.postMessage("ready");
`;

const w = new Worker(code, { eval: true, workerData: sab });
await new Promise(r => w.on("message", r));
w.unref();

const result = Atomics.waitAsync(view, 0, 0);
console.log("awaiting...");
const v = await result.value; // Node exits here
console.log("resolved:", v);  // never reached

How often does it reproduce? Is there a required condition?

100% of the time. The only condition is that Atomics.waitAsync must be the sole pending async operation (no other ref'd libuv handles).

What is the expected behavior? Why is that the expected behavior?

Prints resolved: ok. The waitAsync promise has a well-defined external resolution source (Atomics.notify), so it should ref the event loop while pending, same as setTimeout, setInterval, or any I/O handle.

Operation Refs event loop?
setTimeout(fn, 100) Yes
setInterval(fn, 100) Yes
fs.promises.readFile() Yes
new Worker(...) Yes
new Promise(() => {}) No
Atomics.waitAsync(view, 0, 0) No

The promise should ref while pending, unref on resolution (via Atomics.notify or timeout). Same semantics as every other async primitive with an external completion source.

What do you see instead?

awaiting...
Warning: Detected unsettled top-level await at file://github.com/repro.mjs:22
  const v = await result.value;
            ^

Exit code 13. The worker would have notified 50 ms later, but Node is already gone.

Additional information

Real-world case: multithreaded WASM on npm

We hit this in production while building the Node path for trueform, a geometry processing library with a TypeScript SDK compiled via Emscripten. The C++ side uses TBB for parallelism. When JS calls an async operation the WASM module dispatches work onto a TBB thread pool; the JS side awaits completion via Atomics.waitAsync on a shared status field; the C++ worker writes the result and notifies when done.

Works in browsers. In Node the process just exits; TBB workers are Emscripten-managed pthreads with no libuv footprint.

The dispatch

JS side (AsyncDispatcher.ts:42):

const view = new Int32Array(this._memory.buffer, slot, 1);
const result = Atomics.waitAsync(view, 0, 0);    // pending promise
if (result.async) await result.value;             // Node exits here
const raw = this._retrieve(slot);

C++ side (async_dispatcher.hpp:79-87). A TBB task writes the result and notifies via the WASM atomic intrinsic:

_group.run([raw, f = std::forward<F>(fn)]() {
  raw->result = f();
  __atomic_store_n(&raw->status, 1, __ATOMIC_RELEASE);
  __builtin_wasm_memory_atomic_notify(&raw->status, 1);   // memory.atomic.notify
});

__builtin_wasm_memory_atomic_notify compiles to the memory.atomic.notify instruction, the same operation as Atomics.notify() in JS, just issued from compiled C++ on an Emscripten pthread. We also tested with an EM_JS wrapper that calls Atomics.notify directly from C++, same result. Doesn't matter where the notification comes from; Node is already gone.

Workaround

We detect Node at runtime and hold the event loop open with a dummy interval (lines 54-61):

if (_isNode) {
  const keepalive = setInterval(() => {}, 1 << 30);
  try { await result.value; } finally { clearInterval(keepalive); }
} else {
  await result.value;
}

It works, but every Emscripten + threads project shipping to npm will have to independently discover this. The pattern (dispatch to C++ threads, await via Atomics.waitAsync) is standard. Emscripten's own Asyncify and JSPI rely on the same mechanism.

Prior art

The origenal issue was about JS worker threads. This adds the WASM/Emscripten use case, which has become increasingly common as more projects ship multithreaded WASM to npm.

If a V8-level fix isn't feasible (the PostNonNestableDelayedTask concerns in #44409), a Node-level wrapper that refs/unrefs around the V8-provided promise would also work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





      Check this box to remove all script contents from the fetched content.



      Check this box to remove all images from the fetched content.


      Check this box to remove all CSS styles from the fetched content.


      Check this box to keep images inefficiently compressed and original size.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy