# 21. 错误处理与调试
JS 一直以来被认为是最难调试的语言之一,因为它是动态的,错误经常以令人迷惑的形式抛出,比如:"object expected" 这种消息没有上下文,因此很难理解。ES3 为了改进这个问题,引入了 try/catch 和 throw 语句,以及一些错误类型。如果没有使用 try/catch 捕获错误,浏览器会提示未捕获的错误(Uncaught xxxError)
# 浏览器调试
在 PC 端我们打开开发者工具就可以审查元素、调试样式,查看 JS 相关调试信息/错误信息。在移动端,可以引入 vconsole,但 vconsole 有个缺点就是无法调试 DOM 样式效果。我们可以使用数据线将手机连接到电脑,通过 PC 端的 Safari 或 Chrome 浏览器来调试手机页面效果。
# 在 PC 端调试 iOS Safari 打开的页面
首先我们需要在 iPhone 上做一些设置:
- 打开设置
- 点击 Safari
- 滚动到页面最下面,点击高级
- 勾选网页检查器
使用手机上的 Safari 打开页面,通过数据线将手机连接到电脑。打开 mac 电脑的 Safari,在顶部菜单 开发 => 手机名称 里面可以看到手机上打开的网页,如下图手机 Safari 打开了 baidu.com
点击对应的网页,即可打开 DOM 检查器,console 等调试面板。在里面可以修改手机页面中的 DOM 样式、内容。在 console 中 alert('123'),会应用到移动端的页面中。
参考:iOS 上的 Safari - Safari Web Inspector Guide (opens new window)
# 在 PC 上调试 Android 设备网页内容
将安卓手机通过数据线连接到电脑,打开手机的开发者选项,允许 USB 调试。打开 Chrome 浏览器,访问 chrome://inspect#devices
检测到设备后即可开始调试。注意:数据线问题可能导致找不到手机。
在 Chrome DevTools 面板中,More Tools - Remote devices 已废弃(deprecated),直接访问 chrome://inspect#devices
即可。
This panel has been deprecated in favor of the chrome://inspect/#devices interface, which has equivalent functionality.
- deprecate
[ˈdeprəkeɪt]
deprecated v. 不赞成;弃用;不宜用 - in favor of 有利于;支持;赞同
- equivalent
[ɪˈkwɪvələnt]
相等的;等价的;等效的; - functionality
[ˌfʌŋkʃəˈnæləti]
n. 功能;
参考:
- 远程调试Android设备 | 知乎 (opens new window)
- Android 设备的远程调试入门 | Chrome DevTools | Google Developers (opens new window)
# 错误处理 try/catch/finally
try/catch 可以捕获错误,可以解决出现错误后代码停止向下执行的问题。catch 捕获到错误时,event 会包含如下属性
message
错误信息,所有浏览器都支持description
IE,其值始终等于 messagenumber
IE,内部错误号fileName
Firefox,发生错误所在的文件名lineNumber
Firefox,发生错误所在的行号stack
Firefox/Safari/Chrome,错误的栈跟踪信息line
Safari,行号sourceId
Safari,实际值为 undefinedsourceURL
Safari,发生错误所在的 URL
出于浏览器兼容性考虑,建议仅使用 message 属性,错误信息 event 属性兼容性测试 demo,参见:error 事件 event 属性兼容性测试 demo | Github (opens new window)
try {
// 可能导致的错误代码
console.log('1');
var e;
e.showMessage(); // 这里会出现一个错误,后面的代码不会执行,会执行catch里的代码
console.log('2');
} catch(e) {
// 在错误处理时怎么处理
console.log(e.message) // 打印错误信息
} finally {
console.log('finally')
}
// 1
// Cannot read property 'showMessage' of undefined
// finally
如果加了 finally,try/catch 中的 return 都不会中断 finally 的执行。最终还是会执行 fianlly。下面的例子中,函数会返回 0, 不管正确还是错误,最终都会执行 finally 的内容
function testFinally() {
try {
return 2;
} catch {
return 1;
} finally {
return 0;
}
}
# 8 种错误类型
- Error 基类型,其他错误类型都是继承自该类型,主要用于抛出自定义错误
- InternalError 底层 JS 引擎抛出异常时由浏览器抛出,例如:栈溢出
- EvalError 在使用 eval() 函数异常时会抛出该错误,浏览器实现会有区别 new eval() 会抛出 TypeError,eval 赋值仅 IE 会抛出异常。
- RangeError 范围错误,数值超出范围时,会触发该错误
var item = new Array(-20);
// Uncaught RangeError: Invalid array length
- ReferenceError 参考/引用错误,在找不到对象的情况下会抛出该错误
var a = x // 这里的x未定义
// Uncaught ReferenceError: x is not defined
- SyntaxError 语法错误
[ˈsɪntaks]
let k = 1;
let k = 1; // let重复定义了k,回报语法错误
// Uncaught SyntaxError: Identifier 'k' has already been declared
try() // 这里明显语法错误
// Uncaught SyntaxError: Unexpected token (
- TypeError 类型错误
var k = 1
k.getInfo() // 数字变量,调用了一个函数
// Uncaught TypeError: k.getInfo is not a function
var o = new 10 // 10不是一个构造函数
// Uncaught TypeError: 10 is not a constructor
- URIError 当 encodeURI() 或 decodeURI() 执行时,URI 格式不正确,会导致该错误,基本不会发生该错误,默认会将参数值转为字符串再编码/解码。
# 抛出自定义错误
通过 throw 操作符可以在任何时候抛出异常,如果没有使用 try/catch 捕获会阻止程序向下执行。throw 抛出的异常信息不一定是 Error 类型,信息是由 throw 后面的值指定。这个值的类型不限,try/catch 时 catch 捕获的值就是 throw 后面的值。
throw 123 // Uncaught 123
throw "hello" // Uncaught hello
throw true // Uncaught true
throw { a: 1 } // Uncaught { a: 1 }
try {
throw '123'
} catch(e) {
console.log(e) // '123'
}
使用 throw 抛出自定义错误信息
try {
throw new Error('custom message') // 这里除了可以抛出 Error 类型错误外,还可以抛出其他 7 种错误
} catch(e) {
console.log('error message: ', e.message); // error message: custom message
console.log('error name: ', e.name); // error name: Error // 这里错误类型为 Error
console.log('error fileName: ', e.fileName); // error fileName: undefined
console.log('error lineNumber: ', e.lineNumber); // error lineNumber: undefined
console.log('error columnNumber: ', e.columnNumber); // error columnNumber: undefined
console.log('error stack: ', e.stack); // error stack: Error: custom message at http://127.0.0.1/error.html:11:19
}
# window 的 error 事件
当发生了错误,但没有使用 try/catch 捕获时,会触发在 window 对象上触发 error 事件,该事件有三个参数:
message
错误信息url
错误文件urlline
错误发生的行数
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>error</title>
</head>
<body>
<script type='text/javascript'>
// 如果错误有try-catch捕获,那程序会继续执行,如果没有捕获,会触发error事件,程序不会向下执行,注意:window.onerror要放在最前面
window.onerror = function(message, url, line) {
console.log('message: ' + message) // message: Uncaught TypeError: 类型错误
console.log('url: ' + url) // url: http://127.0.0.1/error.html
console.log('line:' + line) // line:25
}
try {
throw new Error('custom message', 't.js', 10)
} catch(e) {
console.log('error message: ', e.message); // error message: custom message
console.log('error name: ', e.name); // error name: Error
console.log('error fileName: ', e.fileName); // error fileName: undefined
console.log('error lineNumber: ', e.lineNumber); // error lineNumber: undefined
console.log('error columnNumber: ', e.columnNumber); // error columnNumber: undefined
console.log('error stack: ', e.stack); // error stack: Error: custom message at http://127.0.0.1/error.html:11:19
}
throw new TypeError('类型错误') // 这里触发的错误,onerror会触发
cosnole.log('info') // 这里不会执行,浏览器实现会有差异,onerror 处理函数中 return,就类似于 try/catch 了
</script>
</body>
</html>
# 处理错误的策略
常见的错误类型
- 类型转换错误, == 与 === 要注意区分
- TypeError 数据类型错误, 比如非数组,却执行了数组的方法
- 通信错误,URL 中如果包含了另一个 redirect url 参数,需要用 encodeURIComponent() 转码,防止 URL 格式错误
可以使用静态代码分析器,比如 ESLint 等。
# 区分致命错误和非致命错
非致命错误,根据以下一个或多个条件确定:
- 不影响用户的主要任务
- 只影响页面的一部分
- 可以恢复
- 重复相同操作可以消除错误
致命错误,可以通过一个或多个条件确定
- 应用程序根本无法继续运行
- 错误明显影响了用户的主要操作
- 会导致其他连带错误
# 把错误记录到服务器
日志记录,需要写一个接口来上报信息。这里使用了 Image 对象来发送请求,有以下几个好处:
- 所有浏览器都支持Image对象
- 可以避免跨域限制
- 记录错误的过程中出问题的概率比较低
function logError(sev, msg) {
// sev 严重程度
// msg 错误信息
var img = new Image();
img.src = "log.php?sev=" + encodeURIComponent(sev) + "&msg=" + encodeURIComponent(msg)
}
// 开始使用
try {
// 可能发生错误的情况
} catch(e) {
// nonfatal adj. 非致命的 ,fatal [ˈfeɪt(ə)l] 致命的
logError('nonfatal', 'Moudle init failed: ' + e.message)
}
# 调试技术
调试错误可以使用 alert() 弹出警告信息,但 alert() 会阻塞程序向下执行,不推荐使用。一般可以使用 console.log 将消息记录到控制台。另外也可以使用浏览器的断点调试功能,ES5 中新增了 debugger 关键字,用于以程序化的方式下断点。在移动端,由于真机运行不方便调试,vconsole 这个工具拦截了系统的 console, xhr 等,在页面中可视化显示调试信息,是移动端调试的利器。
# 使用 console 把消息记录到控制台
console 除了我们平常使用到的 log() 函数外,还支持 info(),warn(),error()
console.log('一般消息') // 一般消息
console.info('信息性消息') // 信息性消息
console.warn('警告消息') // 警告消息
console.error('错误消息') // 错误消息
# 浏览器断点调试/debugger
chrome 断点调试参见 怎么使用Chrome断点调试功能,断点调试使用场景 - dev-zuo 技术日常 (opens new window)
在 ES5 中,新增了 debugger 关键字,用于以程序化的方式下断点。打开页面时,程序会停留在该位置,并自动打开 DevTools 调试面板,点击按钮(Resume script execution, Resume [rɪˈzjuːm]
)可以继续向下执行
<script>
let a = 123
let b = a ** 2
debugger
let c = a * b
console.log(c)
</script>
运行效果如下图:
# 在页面中打印消息/拦截原生 console
可以自定义一个 log() 函数,在需要记录 log 时调用。log() 函数每次执行时,可以像 dom 指定区域 append 错误信息的 DOM。
vconsole 的思路就是这样,先拦截 console、xhr 等系统方法,在把信息 append 到页面的 DOM 中,方便查看。更多细节参考:为什么vconsole直接new一下就能引入,实现原理是什么? - dev-zuo 技术日常 (opens new window)
# 抛出错误 throw/assert()
抛出错误是调试代码的好方法,只要看一眼错误就能确定错误的确切信息,可以节省额外调试的工作量。
function divide(num1, num2) {
if (typeof num1 != 'number' || typeof num2 != 'number') {
throw new Error('divide(): 所有的参数必须是 number 类型')
}
return num1 / num2
}
// divide(1, 2)
// 0.5
// divide(3, '123')
// Uncaught Error: divide(): 所有的参数必须是 number 类型
// at divide (<anonymous>:3:11)
// at <anonymous>:1:1
一般对于错误比较多的场景,会使用 assert() [əˈsɜːt]
断言函数来封装抛出异常的操作,使抛出自定义错误时代码更加简洁,更好理解。下面使用 assert() 来重构上面的代码
function assert(condition, errorMsg) {
if (condition) {
throw new Error(errorMsg)
}
}
function divide(num1, num2) {
assert(typeof num1 != 'number' || typeof num2 != 'number', 'divide(): 所有的参数必须是 number 类型')
return num1 / num2
}
# 旧版 IE 的常见错误
IE 曾是最难调试 JS 错误的浏览器之一,旧版本抛出的错误通常比较简短,比较含糊,缺少上下文。
- 无效字符 'invalid character',在检测到 JS 文件中存在无效字符时抛出
- 未找到成员 'member not found',旧版 IE 中 DOM 对象都是使用非原生 JS 对象(COM 对象)实现的,在垃圾回收时,可能会导致一些奇怪的行为。如果一个已销毁的对象,给他赋值时会出现该错误。
- 未知运行时错误,比如
<p>
标签内包含另一个块级元素时触发 - 语法错误
- 系统找不到指定资源 'the system cannot locate the resource specified',如果 URL 长度超过了 2083 个字符会发生该错误。