一、开场:错误处理与调试的目的与范围
概念 :前端错误分为三大类------语法/构建期 、运行时 、通信(网络) 。调试的目标是尽早发现 、快速定位 、稳定兜底 、可回溯 。
原理 :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)。
- 非重大错误 :局部功能退化(如次要按钮失效)。
实践:
- 入口守卫:应用启动期 try/catch + 全局 error/unhandledrejection。
- 边界组件(React Error Boundary / Vue errorHandler):避免整页崩溃。
- 重试与降级:网络失败指数退避重试;接口失败展示占位或缓存。
- 提示与上报 :不打扰用户的同时,留可诊断信息(指纹、版本、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; // 让上层决定是否降级
}
}