JavaScript 错误处理与调试全攻略:从 try/catch 到全局兜底

一、开场:错误处理与调试的目的与范围

概念 :前端错误分为三大类------语法/构建期运行时通信(网络) 。调试的目标是尽早发现快速定位稳定兜底可回溯
原理 :JavaScript 单线程,异常若未捕获会中断当前调用栈 ;Promise 异常若未处理,会上报为 unhandledrejection
对比

  • try/catch 负责同步代码异常;
  • .catch / try { await } catch 负责异步异常;
  • window.onerror / error 事件最后一道全局兜底
    实践 :开发期打开 DevTools;线上配合日志/埋点/Source Map。
    拓展 :配合 ESLint/TypeScript 静态发现问题。
    潜在问题:误把 Promise 错误交给 try/catch(未 await),导致漏报。

二、桌面端控制台(如何打开 Inspect / DevTools)

概念 :浏览器开发者工具(DevTools)是前端调试主场。
原理 :它能查看 DOM、样式、网络、断点、运行时、内存、性能。
对比 :Chrome/Edge/Firefox/Safari 的入口与快捷键略有差异。
实践

  • Chrome/Edge/Brave :右键页面 → Inspect ;或按 F12 / Ctrl+Shift+I (macOS:⌥⌘I)。
  • Firefox :右键 → Inspect ;或 F12 / Ctrl+Shift+I (macOS:⌥⌘I)。
  • Safari(macOS) :先在 Safari 偏好设置 → 高级 勾选"在菜单栏显示'开发'菜单";然后 开发 → 显示 JavaScript 控制台⌥⌘C )。
    拓展 :可以把"Pause on exceptions (异常时暂停)"打开,出错即停。
    潜在问题:跨 iframe 调试需切换 Console 的 execution context。

三、移动端控制台(vConsole)

概念vConsole 是在移动网页内嵌的轻量调试面板。
原理 :通过拦截 console.*、网络请求等,渲染到页面内的浮动面板。
对比

  • vConsole:无电脑、无需连线即可看日志;
  • 远程调试:Android 用 Chrome 远程调试,iOS 用 Safari 远程调试,功能更强。
    实践
bash 复制代码
# 安装
npm i vconsole
javascript 复制代码
// main.ts / main.js
// 1) 仅在非生产或条件下启用,避免给用户看到
if (import.meta.env.MODE !== 'production') {
  // 2) 动态引入避免首屏体积增加
  import('vconsole').then(({ default: VConsole }) => {
    // 3) 创建实例即可
    const v = new VConsole(); // 可传入 { maxLogNumber: 1000 } 等参数
    // 4) 可选:自定义插件或面板
    // v.addPlugin(new CustomPlugin(...))
  });
}

拓展 :和你自建的"日志上报"一起用,实现"现场取证"。
潜在问题 :注意仅在调试环境开启,避免性能与安全隐患。


四、错误处理(JavaScript 的几种途径)

概念 :处理链路从局部捕获全局兜底
原理

  • try/catch/finally:同步栈内抛错被捕获;
  • Promise.catch / async-await:异步错误在 microtask 队列完成后抛到 catch;
  • 全局事件window.onerror(脚本/运行时异常),window.onunhandledrejection(未处理的 Promise)。
    对比 :try/catch 更接近业务语义;全局更像黑盒兜底。
    实践 :见后文示例。
    拓展 :配合错误分类重试 策略。
    潜在问题try/catch 不会捕获语法阶段报错(代码解析失败根本执行不到)。

五、try/catch 语句与 finally 的细节

5.1 基本示例(捕获错误)

javascript 复制代码
function divide(a, b) {
  try {
    // 1) 可能抛错的同步逻辑
    if (b === 0) throw new Error('除数不能为 0');
    // 2) 正常返回
    return a / b;
  } catch (e) {
    // 3) 捕获并处理
    console.error('[divide error]:', e.message); // 记录
    return NaN; // 4) 返回一个安全值,避免中断后续流程
  }
}

console.log(divide(6, 3)); // 2
console.log(divide(6, 0)); // NaN(错误被捕获)

5.2 finally 的 return 与 catch 的 return

概念finally 一定会执行 ;如果 finally 里 return,它会覆盖 try 或 catch 的返回/抛错。

javascript 复制代码
function demo(flag) {
  try {
    if (flag) throw new Error('X');
    return 'from-try';           // A: try 的返回
  } catch (e) {
    return 'from-catch';         // B: catch 的返回
  } finally {
    return 'from-finally';       // C: finally 的返回(会覆盖 A 或 B)
  }
}

console.log(demo(false)); // "from-finally"
console.log(demo(true));  // "from-finally"
// 逐行说明:无论 try 或 catch 如何,finally 的 return 都最终生效。
// 实务建议:finally 中**避免 return / throw**,只做清理(关闭句柄、释放资源)。

5.3 try/catch 不会再被系统全局捕获

javascript 复制代码
// 1) 全局兜底
window.addEventListener('error', (evt) => {
  console.log('[global error caught]', evt.error?.message);
});

// 2) 在 try/catch 内部抛错 → 被局部捕获,不再冒泡到 window.onerror
try {
  throw new Error('局部捕获后,window.onerror 不会再收到');
} catch (e) {
  console.log('[local caught]', e.message);
}

六、内置错误类型与使用场景

概念 :常见内置错误及何时出现。
原理 :不同错误类型用于表达不同语义,便于精准处理
对比与实践 (使用 instanceof 验证):

javascript 复制代码
try {
  // Error:通用错误
  // RangeError:数字/长度超出允许范围
  // ReferenceError:访问了未声明的变量
  // SyntaxError:解析 JS 时语法无效(仅能在 eval/Function 中被 try/catch 捕获)
  // TypeError:对类型不支持的操作(如调用 undefined 的方法)
  // URIError:URI 相关函数参数非法(decodeURI/encodeURI)
  // EvalError:历史遗留,几乎不再主动抛出
  // InternalError:引擎内部错误(部分浏览器,如 Firefox),很少见

  // 示例:TypeError
  const x = undefined;
  x.toString(); // 这里会抛 TypeError
} catch (e) {
  if (e instanceof TypeError) {
    console.log('类型错误:', e.message);
  } else if (e instanceof ReferenceError) {
    console.log('引用错误:', e.message);
  } else if (e instanceof RangeError) {
    console.log('范围错误:', e.message);
  } else if (e instanceof SyntaxError) {
    console.log('语法错误:', e.message);
  } else if (e instanceof URIError) {
    console.log('URI 错误:', e.message);
  } else {
    console.log('通用错误:', e.message);
  }
}

拓展 :自定义业务错误类(见 § 八)。
潜在问题SyntaxError 一般在解析阶段 就报,除非放到 eval 里。


七、抛出错误(throw)

7.1 何时使用 throw

概念 :当输入非法状态异常不符合业务不变量 时,主动 throw 明确失败。
原理 :早失败(fail fast)让错误更接近根因。
实践 :参数校验、断言、不可恢复错误。
潜在问题 :频繁 throw 却不捕获,会让用户看到白屏;应在边界层集中捕获与上报

7.2 自定义错误并捕获

javascript 复制代码
// 1) 自定义错误类型,便于分类与识别
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

function createUser(payload) {
  // 2) 参数校验失败 → 主动抛出
  if (!payload.email) throw new ValidationError('email 必填', 'email');
  // 3) 正常逻辑...
  return { id: Date.now(), ...payload };
}

try {
  createUser({ name: 'Lee' }); // 缺 email,抛错
} catch (e) {
  if (e instanceof ValidationError) {
    console.warn('表单校验失败:', e.field, e.message);
  } else {
    console.error('未知错误:', e);
  }
}

八、error 与 unhandledrejection 事件(最后一道防线)

javascript 复制代码
// 1) 捕获脚本运行时未处理错误
window.addEventListener('error', (evt) => {
  // evt.message / evt.filename / evt.lineno / evt.colno / evt.error?.stack
  console.error('[window.onerror]', evt.message, evt.filename, evt.lineno, evt.colno);
});

// 2) 捕获未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', (evt) => {
  // evt.reason 可能是 Error 或任意值
  console.error('[unhandledrejection]', evt.reason);
});

拓展 :这里适合做用户提示错误上报
潜在问题 :跨域脚本若未加 crossorigin 与正确的 CORS 与 Source Map,stack 可能被屏蔽(变成 "Script error.")。


九、错误处理策略(实战清单)

概念 :按严重度分层处理。
原理可恢复不可恢复 分流;可重试不可重试 区分。
对比

  • 重大错误:导致页面不可用/数据错乱(如初始化失败、关键接口 401/500)。
  • 非重大错误 :局部功能退化(如次要按钮失效)。
    实践
  1. 入口守卫:应用启动期 try/catch + 全局 error/unhandledrejection。
  2. 边界组件(React Error Boundary / Vue errorHandler):避免整页崩溃。
  3. 重试与降级:网络失败指数退避重试;接口失败展示占位或缓存。
  4. 提示与上报 :不打扰用户的同时,留可诊断信息(指纹、版本、stack)。
    拓展 :灰度开关(仅对少量用户启用新功能)。
    潜在问题 :过度"吃掉"错误导致沉默失败,难以诊断。

十、识别与分类常见错误

10.1 静态代码分析器

概念 :在运行前发现问题。
工具ESLint (语法/风格/潜在 bug),TypeScript (类型安全),Prettier (格式)。
实践 :CI 阶段 eslint .tsc --noEmit
潜在问题 :类型断言 as any 滥用会掩盖风险。

10.2 类型转换错误(coercion)

sql 复制代码
// 逐行示例
console.log(Number('12a')); // NaN:数字转换失败
console.log('5' - 1);       // 4:'-' 触发数值转换
console.log('5' + 1);       // "51":'+' 触发字符串拼接
console.log(Boolean(''));   // false;非空字符串为 true
console.log([] == 0);       // true:抽象相等导致诡异结果 → 避免 '==',使用 '==='

实践建议一律用 === ;解析数字用 Number()/parseInt(str, 10) 并显式校验 Number.isNaN
潜在问题parseInt('08') 不写基数在老环境可能当作八进制。

10.3 数据类型错误(TypeError 典型)

javascript 复制代码
const user = null;
// console.log(user.name) // TypeError: Cannot read properties of null
console.log(user?.name ?? '匿名'); // 使用可选链与空值合并避免崩

10.4 通信错误(网络)

javascript 复制代码
// 带超时与重试的 fetch 封装(精简版,逐行注释)
async function fetchWithRetry(url, { retries = 2, timeout = 8000, ...options } = {}) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    const controller = new AbortController();            // 1) 用于超时中止
    const tid = setTimeout(() => controller.abort(), timeout);
    try {
      const res = await fetch(url, { signal: controller.signal, ...options });
      clearTimeout(tid);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();                           // 2) 正常解析
    } catch (e) {
      clearTimeout(tid);
      if (attempt === retries) throw e;                  // 3) 到顶仍失败 → 抛出
      await new Promise(r => setTimeout(r, 2 ** attempt * 300)); // 4) 指数退避
    }
  }
}

十一、把错误推送到服务器(上报)

概念 :线上问题可观测性
原理 :将 message/stack/url/行列号/用户环境 发送到日志服务。
实践

php 复制代码
function reportError(payload) {
  // 1) 优先使用 sendBeacon(页面关闭时也尽力发送)
  const url = '/log/error';
  const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
  if (navigator.sendBeacon && navigator.sendBeacon(url, blob)) return;

  // 2) 回退到 fetch keepalive
  fetch(url, { method: 'POST', body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, keepalive: true })
    .catch(() => {}); // 3) 上报失败不影响用户
}

// 全局兜底接入
window.addEventListener('error', (evt) => {
  reportError({
    type: 'error',
    message: evt.message,
    filename: evt.filename,
    lineno: evt.lineno,
    colno: evt.colno,
    stack: evt.error?.stack,
    ua: navigator.userAgent,
    time: Date.now(),
  });
});

window.addEventListener('unhandledrejection', (evt) => {
  reportError({
    type: 'unhandledrejection',
    reason: String(evt.reason?.message || evt.reason),
    stack: evt.reason?.stack,
    time: Date.now(),
  });
});

好处 :复现难题可回溯;可统计发生率影响面
潜在问题:注意隐私合规(脱敏)、采样率与流量控制。


十二、调试技术速写

概念/实践

  • 断点 :普通、条件(i > 100)、事件断点(点击、XHR)。
  • Call Stack:回溯调用链。
  • Scope/Watch:观察变量。
  • Source Map:把产物映射回源码。
  • 黑盒(Blackbox) :忽略第三方库栈帧。
  • 性能面板 :找卡顿与长任务。
    潜在问题:生产包若去除了 Source Map,上线排障难度增加。

十三、把消息记录到控制台(log/warn/error)

概念 :三者仅语义与展示级别 不同。
实践

javascript 复制代码
console.log('普通信息');        // 用于状态、流程、变量
console.warn('可能有问题');     // 用于弃用、边界情况
console.error('确定出错');      // 用于真实错误或异常路径

拓展 :配合 console.group / groupEnd 组织日志;console.table 展示列表。
潜在问题:上线应减少噪音日志;可按环境变量开关。


十四、理解控制台运行时与 $0

概念 :Console 有执行上下文 (页面/iframe/扩展)。
实践

  • DevTools Elements 面板选中的元素, ** <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 ∗ ∗ 指向它; ' 0** 指向它;` </math>0∗∗指向它;'1$2` 是历史所选。
  • $()document.querySelector 的别名,$$()querySelectorAll 的别名(返回数组)。
    潜在问题:上下文选错会"找不到变量/元素"。

十五、使用 JavaScript 调试器(debugger

概念debugger 语句等同于编程式断点
实践

ini 复制代码
function complexCalc(a, b) {
  const mid = a * b;
  debugger; // 打开 DevTools 时在此停下,可检查 mid、调用栈、作用域
  return mid + 42;
}
complexCalc(3, 7);

拓展 :可配合条件:if (x > 100) debugger;
潜在问题 :记得删除 或用构建工具在生产环境剔除


十六、在页面中打印消息(UI Log)

概念 :当控制台不可用(某些 App 内置 WebView),在页面上显示关键日志。
实践

css 复制代码
// 简易屏上日志面板,逐行注释
(function () {
  const box = document.createElement('pre');
  box.style.cssText = 'position:fixed;left:0;bottom:0;max-height:40%;overflow:auto;width:100%;background:rgba(0,0,0,.7);color:#fff;padding:8px;margin:0;font:12px/1.4 monospace;z-index:99999';
  document.body.appendChild(box);
  const log = (...args) => (box.textContent += args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n');
  log('屏上日志已就绪');
  window.__screenLog = log; // 暴露给外部使用
})();
__screenLog('Hello, Mobile!');

潜在问题:注意性能和隐私,调试完记得移除。


十七、补充:API 代理 console(把日志发到移动端/服务器)

javascript 复制代码
// 代理 console,把日志同时输出到 vConsole/屏上面板/服务器
(function () {
  const raw = { log: console.log, warn: console.warn, error: console.error };

  function send(type, args) {
    try {
      fetch('/log/console', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        keepalive: true,
        body: JSON.stringify({ type, args: Array.from(args).map(String), time: Date.now() }),
      });
    } catch (_) {}
  }

  ['log', 'warn', 'error'].forEach((k) => {
    console[k] = function (...args) {
      raw[k].apply(console, args); // 1) 原样输出到控制台
      if (window.__screenLog) window.__screenLog(`[${k}]`, ...args); // 2) 屏上
      send(k, args); // 3) 服务器
    };
  });
})();

潜在问题 :注意循环日志(代理里调用 console 又触发代理);上面用 raw 保存了原方法避免递归。


十八、用 throw 做断言(快速测试)

javascript 复制代码
function assert(cond, msg = '断言失败') {
  if (!cond) throw new Error(msg); // 不符合预期时抛错
}

// 用法示例(逐行注释)
function add(a, b) {
  assert(typeof a === 'number' && typeof b === 'number', 'add 需要数字');
  return a + b;
}

add(1, 2);       // 3
add('1', 2);     // 抛错:断言失败 → 立刻暴露问题

实践:只在开发/测试使用,生产要么移除要么转为稳态校验。


十九、常见特殊报错释义与排查

19.1 无效字符(Invalid or unexpected token)

场景 :拷贝了"花式引号"、BOM、零宽字符、中文逗号到代码里。
排查

  • 用编辑器"显示不可见字符";
  • 统一保存为 UTF-8,无 BOM;
  • 替换" "为 " ",,

19.2 未找到成员(找不到属性/方法)

表现

  • Cannot read properties of undefined (reading 'x')
  • xxx is not a function
    应对 :可选链 obj?.x、默认值 ??、在使用前校验类型;模块导入名写错也会导致 is not a function

19.3 未知的运行错误(Generic Error)

表现 :没有特定类型,只是 Error: something bad
应对 :打印 stack 、追踪最近改动、用断点重现场景;必要时增加更多业务日志

19.4 语法错误(SyntaxError)

表现 :构建或运行前就无法解析;或 eval('foo bar')
应对 :交给构建工具(Babel/TS),或把可能有问题的动态代码放入 try/catch 包裹的 eval,仅用于受控场景。

19.5 系统找不到指定资源(资源加载失败)

表现Failed to load resource: net::ERR_NAME_NOT_RESOLVED / 404,脚本或样式加载失败。
应对 :检查 URL、网络、CDN、CORS、crossorigin、SRI;对关键资源做回退重试


二十、try/catch 与异步的正确姿势(重要!)

javascript 复制代码
// 错误示例:这里的 try/catch 捕不到 setTimeout 里的错误
try {
  setTimeout(() => { throw new Error('异步错误'); }, 0);
} catch (e) {
  // 永远不会执行到这里
}

// 正确:用 Promise + catch,或在 async 函数中 await
async function main() {
  try {
    await new Promise((_, rej) => setTimeout(() => rej(new Error('异步失败')), 0));
  } catch (e) {
    console.log('已捕获异步错误:', e.message);
  }
}
main();

要点 :**异步要么 .catch(),要么 await + try/catch;否则落到 unhandledrejection


二十一、重大 / 非重大错误的区分(决策表)

概念 :按影响范围可恢复性 打标签。
简表

  • Fatal(重大) :无法继续(入口数据/鉴权失败/主界面渲染失败)→ 立即告警 + 跳转登录/维护页 + 上报。
  • High(较重) :核心流程受阻(支付失败、编辑器崩)→ 提示 + 重试/兜底 + 上报。
  • Low(一般) :局部功能异常(下载按钮失败)→ toast + 重试/降级。
  • Info(提示) :非错误,记录用于分析。

二十二、控制台运行时与页面快速打印回顾($0、debugger、UI Log)

(此节为前述 13~16 小结,便于查阅)

  • $0:Elements 选中元素的引用。
  • debugger:编程断点。
  • UI Log:当 console 不可用时把关键信息打印到页面。

二十三、整套最小可用模板(拿去即用)

javascript 复制代码
// 1) 启动期兜底
window.addEventListener('error', (e) => reportError({ type: 'error', message: e.message, stack: e.error?.stack }));
window.addEventListener('unhandledrejection', (e) => reportError({ type: 'unhandledrejection', reason: String(e.reason), stack: e.reason?.stack }));

// 2) 业务入口守卫
async function bootstrap() {
  try {
    await initConfig();             // 关键配置
    await initAuth();               // 鉴权
    await initApp();                // 渲染
  } catch (e) {
    showFatalScreen('系统初始化失败,请刷新重试'); // 用户可见兜底
    reportError({ type: 'bootstrap', message: e.message, stack: e.stack });
  }
}
bootstrap();

// 3) fetch 包装(失败重试 + 统一处理)
async function api(url, opts) {
  try {
    return await fetchWithRetry(url, { retries: 2, timeout: 8000, ...opts });
  } catch (e) {
    notify('网络开小差,已为你重试'); // 轻提示
    reportError({ type: 'api', url, message: e.message });
    throw e; // 让上层决定是否降级
  }
}
相关推荐
zm43513 分钟前
浅记Monaco-editor 初体验
前端
超凌16 分钟前
vue element-ui 对表格的单元格边框加粗
前端
前端搬运侠18 分钟前
🚀 TypeScript 中的 10 个隐藏技巧,让你的代码更优雅!
前端·typescript
CodeTransfer19 分钟前
css中animation与js的绑定原来还能这样玩。。。
前端·javascript
liming49520 分钟前
运行node18报错
前端
202622 分钟前
14.7 企业级脚手架-制品仓库发布使用
前端·vue.js
coding随想29 分钟前
揭秘HTML5的隐藏开关:监控资源加载状态readyState属性全解析!
前端
coding随想37 分钟前
等待页面加载事件用window.onload还是DOMContentLoaded,一文给你讲清楚
前端
大舔牛37 分钟前
Viewport 与移动端 1px 问题解析
前端·面试