Node 26.12.0 加载钩子报 ERR_INTERNAL_ASSERTION,检查 hook 内的 require(ESM)
Node 26.12.0 在 module.register 加载的 hook 内同步 require 带导入的 ESM 时触发内部断言,说明识别条件、局部导入调整及修复发行边界。
程序升级到 Node.js 26.12.0 后,还没进入业务入口,就在 ModuleJobSync.syncLink 报 ERR_INTERNAL_ASSERTION。如果启动器使用了 module.register(),而它注册的 hook 模块又通过 createRequire() 加载 ESM,应先检查这条组合。已确认的触发环境为 Linux x64;相同加载方式在 26.11.1、26.10.0 正常,不能把结论扩大成所有 ESM 导入都失败。
这个分支有两个同时成立的条件:require() 发生在 hook 模块的加载阶段,被加载的 ESM 至少还有一条 import。hook 文件甚至不必导出 resolve 或 load,仅初始化过程就足以触发。
调用栈里的 hook 线程是关键
module.register() 注册的异步加载钩子在独立线程中运行。主线程里能成功的 require(esm),不能据此证明 hook 线程采用了同一条内部路径。
下面用三个文件说明触发结构,均为示例文件:
// dep.mjs
import 'node:fs';
export const value = 1;
// hooks.mjs
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const { value } = require('./dep.mjs');
// app.mjs
import { register } from 'node:module';
register(new URL('./hooks.mjs', import.meta.url));
沿真实错误栈找到 hook 文件,确认 require 的目标及其静态依赖。不要只搜索应用入口,也不要把所有异步函数都当成顶层 await。若报的是 ERR_REQUIRE_ASYNC_MODULE,且模块图确实带顶层 await,应走同步 require 遇到异步模块图的处理路径。本文的触发文件可以完全没有顶层 await。
自有 hook 可以先去掉这处同步跨模块加载
若被加载的是自己维护的 ESM,且 hook 不依赖 CommonJS 特有的同步返回或条件导出选择,可以将上例的 hooks.mjs 改成静态 ESM 导入:
// hooks.mjs
import { value } from './dep.mjs';
保留文件其余真正需要的 hook 导出。这段只展示加载入口的改法,不是要把现有 hook 清空。静态 import 会参与模块链接,命名导出也应与目标模块一致,不能机械增加 .default。如果 require 来自第三方启动器,应优先使用它支持的配置或兼容版本,直接改 node_modules 很容易在重装后丢失。
暂时无法调整加载器时,可在项目的 Node 版本约束、CI 和服务启动环境中恢复已验证兼容的组合;26.11.1 是这一具体回归的对照版本,不是长期固定运行时的建议。先核对当前安全更新与依赖支持范围,保存锁文件和完整启动参数,避免只切换终端 Node,服务进程仍使用26.12.0。
合并修复与正式发行要分开核对
问题出在 hook 线程内的同步模块链接:本应同步得到的依赖任务走到了返回 Promise 的路径,触发内部断言。修复已于2026年10月17日合入,调整了 hook 线程上 require 相关请求的解析与任务选择,并增加相同结构的回归检查。
截至2026年10月20日,已核对的最新26系正式版仍是10月14日发布的26.12.0,不能把合并日期当成修复版号。后续更新前应确认目标正式版本确实包含该修复。
恢复后从原启动器运行,检查 hook 初始化、模块导出及业务入口均完成,再验证原有加载规则。只去掉断言,却跳过了需要的 hook,仍可能让模块解析悄悄改变。若撤回改动,应一起恢复 hook 文件、运行版本配置与启动参数。


