错误类型与自定义错误
错误类型与自定义错误
JavaScript定义了多种内置错误类型。本文将介绍这些错误类型的特点,以及如何创建和使用自定义错误类型。
JavaScript内置错误类型
JavaScript提供了几种标准的内置错误类型,所有这些错误类型都继承自基础的Error
对象。
Error
Error
是所有错误的基类,其他特定类型的错误都继承自它。通常用于创建自定义错误。
const error = new Error('这是一个一般性错误');
console.log(error.name); // "Error"
console.log(error.message); // "这是一个一般性错误"
console.log(error.stack); // 包含堆栈跟踪信息的字符串
SyntaxError
当JavaScript引擎解析代码时遇到语法错误时抛出。
// 这会导致SyntaxError
// function myFunction( {
// console.log('缺少右括号');
// }
try {
eval('if (true) { console.log("缺少右花括号"');
} catch (error) {
console.log(error.name); // "SyntaxError"
console.log(error.message); // 通常包含有关语法错误的详细信息
}
ReferenceError
当引用不存在的变量时抛出。
try {
console.log(undefinedVariable); // 变量未定义
} catch (error) {
console.log(error.name); // "ReferenceError"
console.log(error.message); // "undefinedVariable is not defined"
}
TypeError
当值的类型与预期不符时抛出。
try {
const str = "Hello";
str.push("World"); // 字符串没有push方法
} catch (error) {
console.log(error.name); // "TypeError"
console.log(error.message); // "str.push is not a function"
}
RangeError
当数值超出有效范围时抛出。
try {
const arr = new Array(-1); // 数组长度不能为负数
} catch (error) {
console.log(error.name); // "RangeError"
console.log(error.message); // "Invalid array length"
}
try {
(10).toFixed(101); // toFixed()参数必须在0到100之间
} catch (error) {
console.log(error.name); // "RangeError"
console.log(error.message); // "toFixed() digits argument must be between 0 and 100"
}
URIError
当URI处理函数(如encodeURI()
、decodeURI()
等)使用不当时抛出。
try {
decodeURIComponent('%'); // 无效的URI编码
} catch (error) {
console.log(error.name); // "URIError"
console.log(error.message); // "URI malformed"
}
EvalError
在早期JavaScript版本中,当eval()
函数使用不当时抛出。在现代JavaScript中很少见,大多数情况下会抛出SyntaxError
或其他错误类型。
// 在现代JavaScript中,eval错误通常是SyntaxError
try {
eval('if (true) { console.log("缺少右花括号"');
} catch (error) {
console.log(error.name); // 通常是"SyntaxError",而不是"EvalError"
}
AggregateError (ES2021)
ES2021引入的新错误类型,用于表示多个错误的集合。通常与Promise.any()
一起使用。
try {
throw new AggregateError([
new Error('错误1'),
new Error('错误2')
], '多个错误发生');
} catch (error) {
console.log(error.name); // "AggregateError"
console.log(error.message); // "多个错误发生"
console.log(error.errors); // [Error: '错误1', Error: '错误2']
}
错误对象的属性和方法
所有错误对象都继承自Error.prototype
,因此具有以下共同属性:
name
表示错误类型的名称。
const error = new TypeError('类型错误示例');
console.log(error.name); // "TypeError"
message
包含错误的详细描述信息。
const error = new Error('这是错误消息');
console.log(error.message); // "这是错误消息"
stack
非标准但被广泛支持的属性,包含错误发生时的堆栈跟踪信息。
function causeError() {
throw new Error('示例错误');
}
try {
causeError();
} catch (error) {
console.log(error.stack);
// 输出类似:
// Error: 示例错误
// at causeError (file.js:2:9)
// at file.js:6:3
}
cause (ES2022)
ES2022引入的新属性,用于指示导致当前错误的原因(通常是另一个错误)。
try {
try {
throw new Error('原始错误');
} catch (originalError) {
throw new Error('包装错误', { cause: originalError });
}
} catch (wrappedError) {
console.log(wrappedError.message); // "包装错误"
console.log(wrappedError.cause.message); // "原始错误"
}
创建自定义错误类型
在复杂应用中,创建自定义错误类型可以帮助更精确地识别和处理特定类型的错误。
使用ES6类语法
最现代的方法是使用ES6的类语法,继承内置的Error
类:
class ValidationError extends Error {
constructor(message, field) {
super(message); // 调用父类构造函数
this.name = 'ValidationError'; // 设置错误名称
this.field = field; // 添加自定义属性
// 修复堆栈跟踪
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ValidationError);
}
}
}
// 使用自定义错误
try {
const username = '';
if (!username) {
throw new ValidationError('用户名不能为空', 'username');
}
} catch (error) {
if (error instanceof ValidationError) {
console.log(`验证错误 (${error.field}): ${error.message}`);
} else {
console.log('其他错误:', error.message);
}
}
使用原型继承(ES5及更早版本)
在不支持ES6类语法的环境中,可以使用传统的原型继承:
function DatabaseError(message, code) {
// 调用Error构造函数
Error.call(this, message);
// 设置错误名称和自定义属性
this.name = 'DatabaseError';
this.message = message;
this.code = code;
// 创建堆栈跟踪
if (Error.captureStackTrace) {
Error.captureStackTrace(this, DatabaseError);
} else {
this.stack = (new Error()).stack;
}
}
// 设置原型链
DatabaseError.prototype = Object.create(Error.prototype);
DatabaseError.prototype.constructor = DatabaseError;
// 使用自定义错误
try {
throw new DatabaseError('数据库连接失败', 'DB_CONN_ERROR');
} catch (error) {
if (error instanceof DatabaseError) {
console.log(`数据库错误 (${error.code}): ${error.message}`);
}
}
创建错误层次结构
在大型应用中,可能需要创建错误类型的层次结构,以便更精细地处理不同类别的错误:
// 基础应用错误
class AppError extends Error {
constructor(message) {
super(message);
this.name = 'AppError';
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AppError);
}
}
}
// 网络相关错误
class NetworkError extends AppError {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
// API错误
class ApiError extends NetworkError {
constructor(message, statusCode, endpoint) {
super(message, statusCode);
this.name = 'ApiError';
this.endpoint = endpoint;
}
}
// 使用错误层次结构
try {
throw new ApiError('获取用户数据失败', 404, '/api/users/123');
} catch (error) {
if (error instanceof ApiError) {
console.log(`API错误 (${error.statusCode}): ${error.message}, 端点: ${error.endpoint}`);
} else if (error instanceof NetworkError) {
console.log(`网络错误 (${error.statusCode}): ${error.message}`);
} else if (error instanceof AppError) {
console.log(`应用错误: ${error.message}`);
} else {
console.log(`未知错误: ${error.message}`);
}
}
自定义错误的最佳实践
1. 提供有意义的错误名称和消息
// 不推荐
throw new Error('错误');
// 推荐
throw new ValidationError('用户名长度必须至少为3个字符', 'username');
2. 包含相关上下文信息
class ConfigError extends Error {
constructor(message, configKey, configValue) {
super(message);
this.name = 'ConfigError';
this.configKey = configKey;
this.configValue = configValue;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ConfigError);
}
}
}
throw new ConfigError(
'配置值无效',
'maxRetries',
-1
);
3. 使用instanceof检查错误类型
try {
// 可能抛出不同类型错误的代码
} catch (error) {
if (error instanceof ValidationError) {
// 处理验证错误
} else if (error instanceof DatabaseError) {
// 处理数据库错误
} else {
// 处理其他类型的错误
}
}
4. 保持错误类型的一致性
在整个应用中使用一致的错误类型命名和结构:
// 所有验证错误都使用相同的结构
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ValidationError);
}
}
}
// 所有HTTP错误都使用相同的结构
class HttpError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'HttpError';
this.statusCode = statusCode;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, HttpError);
}
}
}
5. 错误代码常量
对于需要在多处使用的错误代码,定义常量可以提高一致性:
// 错误代码常量
const ErrorCodes = {
VALIDATION: {
REQUIRED_FIELD: 'REQUIRED_FIELD',
INVALID_FORMAT: 'INVALID_FORMAT',
OUT_OF_RANGE: 'OUT_OF_RANGE'
},
NETWORK: {
CONNECTION_FAILED: 'CONNECTION_FAILED',
TIMEOUT: 'TIMEOUT',
UNAUTHORIZED: 'UNAUTHORIZED'
}
};
class FormError extends Error {
constructor(message, field, code) {
super(message);
this.name = 'FormError';
this.field = field;
this.code = code;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, FormError);
}
}
}
// 使用错误代码
function validateEmail(email) {
if (!email) {
throw new FormError(
'邮箱不能为空',
'email',
ErrorCodes.VALIDATION.REQUIRED_FIELD
);
}
if (!email.includes('@')) {
throw new FormError(
'邮箱格式无效',
'email',
ErrorCodes.VALIDATION.INVALID_FORMAT
);
}
}
实际应用示例
表单验证
class FormValidationError extends Error {
constructor(message, field, value) {
super(message);
this.name = 'FormValidationError';
this.field = field;
this.value = value;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, FormValidationError);
}
}
}
function validateForm(formData) {
// 验证用户名
if (!formData.username) {
throw new FormValidationError('用户名不能为空', 'username', formData.username);
}
if (formData.username.length < 3) {
throw new FormValidationError(
'用户名长度不能少于3个字符',
'username',
formData.username
);
}
// 验证密码
if (!formData.password) {
throw new FormValidationError('密码不能为空', 'password', '');
}
if (formData.password.length < 8) {
throw new FormValidationError(
'密码长度不能少于8个字符',
'password',
'***' // 不显示实际密码
);
}
// 验证邮箱
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email) {
throw new FormValidationError('邮箱不能为空', 'email', formData.email);
}
if (!emailRegex.test(formData.email)) {
throw new FormValidationError('邮箱格式无效', 'email', formData.email);
}
// 所有验证通过
return true;
}
// 使用示例
try {
const formData = {
username: 'ab', // 用户名太短
password: 'password123',
email: 'user@example.com'
};
validateForm(formData);
console.log('表单验证通过');
} catch (error) {
if (error instanceof FormValidationError) {
console.log(`验证失败: ${error.field} - ${error.message}`);
console.log(`提交的值: ${error.value}`);
} else {
console.log('发生未知错误:', error);
}
}
API错误处理
在前端应用中处理API请求错误是一个常见场景,使用自定义错误可以更好地组织和处理这些错误:
// API错误层次结构
class ApiError extends Error {
constructor(message, status = 500) {
super(message);
this.name = 'ApiError';
this.status = status;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ApiError);
}
}
}
// 特定类型的API错误
class NotFoundError extends ApiError {
constructor(resource, id) {
super(`${resource} with id ${id} not found`, 404);
this.name = 'NotFoundError';
this.resource = resource;
this.resourceId = id;
}
}
class AuthenticationError extends ApiError {
constructor(message = '认证失败') {
super(message, 401);
this.name = 'AuthenticationError';
}
}
class ForbiddenError extends ApiError {
constructor(message = '没有权限执行此操作') {
super(message, 403);
this.name = 'ForbiddenError';
}
}
// API请求函数
async function fetchResource(resourceType, id) {
try {
const response = await fetch(`https://api.example.com/${resourceType}/${id}`);
if (response.status === 404) {
throw new NotFoundError(resourceType, id);
}
if (response.status === 401) {
throw new AuthenticationError();
}
if (response.status === 403) {
throw new ForbiddenError();
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ApiError(
errorData.message || `请求失败: ${response.status} ${response.statusText}`,
response.status
);
}
return await response.json();
} catch (error) {
// 重新抛出网络错误作为ApiError
if (!(error instanceof ApiError)) {
if (error.name === 'TypeError' && error.message.includes('fetch')) {
throw new ApiError('网络连接失败,请检查您的互联网连接', 0);
}
throw new ApiError(error.message);
}
throw error;
}
}
// 使用示例
async function loadUserProfile(userId) {
try {
const user = await fetchResource('users', userId);
displayUserProfile(user);
} catch (error) {
if (error instanceof NotFoundError) {
showMessage(`用户 ${error.resourceId} 不存在`);
} else if (error instanceof AuthenticationError) {
redirectToLogin();
} else if (error instanceof ForbiddenError) {
showMessage('您没有权限查看此用户资料');
} else if (error instanceof ApiError) {
showMessage(`加载用户资料时出错: ${error.message}`);
} else {
showMessage('发生未知错误');
console.error(error);
}
}
}
异步操作超时处理
创建一个超时错误类型,用于处理异步操作超时:
class TimeoutError extends Error {
constructor(operation, timeoutMs) {
super(`操作 "${operation}" 超时 (${timeoutMs}ms)`);
this.name = 'TimeoutError';
this.operation = operation;
this.timeoutMs = timeoutMs;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, TimeoutError);
}
}
}
// 带超时的Promise包装器
function withTimeout(promise, timeoutMs, operationName) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new TimeoutError(operationName, timeoutMs));
}, timeoutMs);
promise
.then(result => {
clearTimeout(timeoutId);
resolve(result);
})
.catch(error => {
clearTimeout(timeoutId);
reject(error);
});
});
}
// 使用示例
async function fetchDataWithTimeout() {
try {
const result = await withTimeout(
fetch('https://api.example.com/data'),
5000, // 5秒超时
'获取数据'
);
return await result.json();
} catch (error) {
if (error instanceof TimeoutError) {
console.error(`请求超时: ${error.message}`);
// 可以选择重试或使用缓存数据
return getCachedData();
}
throw error; // 重新抛出其他错误
}
}
错误处理策略
集中式错误处理
在大型应用中,可以创建一个集中式错误处理器来统一处理不同类型的错误:
// 错误处理器
class ErrorHandler {
constructor() {
this.handlers = new Map();
// 注册默认处理器
this.registerHandler(Error, this.defaultHandler);
}
// 注册特定错误类型的处理器
registerHandler(errorType, handler) {
this.handlers.set(errorType, handler);
}
// 处理错误
handleError(error, context = {}) {
// 查找最匹配的错误处理器
let currentErrorType = error.constructor;
let handler = null;
while (currentErrorType !== null) {
handler = this.handlers.get(currentErrorType);
if (handler) break;
// 向上查找原型链
currentErrorType = Object.getPrototypeOf(currentErrorType);
}
// 如果没有找到处理器,使用默认处理器
if (!handler) {
handler = this.defaultHandler;
}
return handler(error, context);
}
// 默认错误处理器
defaultHandler(error, context) {
console.error('未处理的错误:', error);
return {
success: false,
message: '发生未知错误,请稍后再试',
error: process.env.NODE_ENV === 'development' ? error : undefined
};
}
}
// 创建全局错误处理器实例
const errorHandler = new ErrorHandler();
// 注册特定错误类型的处理器
errorHandler.registerHandler(ValidationError, (error, context) => {
return {
success: false,
field: error.field,
message: error.message,
code: 'VALIDATION_ERROR'
};
});
errorHandler.registerHandler(ApiError, (error, context) => {
// 记录API错误
logApiError(error, context);
if (error.status === 401) {
// 处理认证错误
redirectToLogin();
return { success: false, redirected: true };
}
return {
success: false,
message: `服务器错误: ${error.message}`,
status: error.status,
code: 'API_ERROR'
};
});
// 使用错误处理器
function processUserData(userData) {
try {
// 处理用户数据...
if (!isValid(userData)) {
throw new ValidationError('无效的用户数据', 'userData');
}
return { success: true, data: processedData };
} catch (error) {
return errorHandler.handleError(error, { userData });
}
}
错误恢复策略
在某些情况下,我们可能希望从错误中恢复并继续执行:
class RetryableError extends Error {
constructor(message, maxRetries = 3) {
super(message);
this.name = 'RetryableError';
this.maxRetries = maxRetries;
this.retryCount = 0;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, RetryableError);
}
}
canRetry() {
return this.retryCount < this.maxRetries;
}
incrementRetryCount() {
this.retryCount++;
return this.retryCount;
}
}
// 使用可重试错误
async function fetchWithRetry(url, options = {}) {
const error = new RetryableError(`获取 ${url} 失败`);
while (true) {
try {
return await fetch(url, options);
} catch (fetchError) {
if (error.canRetry()) {
const retryCount = error.incrementRetryCount();
console.log(`尝试 ${retryCount}/${error.maxRetries} 失败,正在重试...`);
// 指数退避策略
const delay = Math.pow(2, retryCount) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
} else {
error.cause = fetchError;
throw error;
}
}
}
}
浏览器和Node.js中的错误处理差异
浏览器特有的错误处理
在浏览器环境中,有一些特有的错误处理机制:
// 全局错误处理
window.onerror = function(message, source, lineno, colno, error) {
console.error('全局错误:', {
message,
source,
lineno,
colno,
error
});
// 向服务器报告错误
reportErrorToServer({
type: error ? error.name : 'Unknown',
message,
source,
lineno,
colno,
stack: error ? error.stack : null
});
// 返回true表示错误已处理
return true;
};
// 处理未捕获的Promise拒绝
window.addEventListener('unhandledrejection', function(event) {
console.error('未处理的Promise拒绝:', event.reason);
// 阻止默认处理
event.preventDefault();
// 向服务器报告错误
reportErrorToServer({
type: 'UnhandledPromiseRejection',
message: event.reason ? event.reason.message : 'Promise被拒绝',
stack: event.reason ? event.reason.stack : null
});
});
Node.js特有的错误处理
Node.js提供了一些特有的错误处理机制:
// 处理未捕获的异常
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error);
// 记录错误
logErrorToFile(error);
// 优雅地关闭应用
gracefulShutdown()
.then(() => {
process.exit(1);
})
.catch(() => {
process.exit(2);
});
});
// 处理未处理的Promise拒绝
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
// 在Node.js 15+中,未处理的Promise拒绝将导致进程退出
// 在早期版本中,可以选择退出进程或仅记录错误
logErrorToFile(reason);
});
// 处理进程即将退出
process.on('exit', (code) => {
console.log(`进程即将退出,退出码: ${code}`);
// 执行最终清理
// 注意:这里只能执行同步操作
});
总结
自定义错误类型是构建健壮JavaScript应用的重要工具。通过创建特定的错误类型,我们可以:
- 提高代码可读性:错误类型的名称可以清晰地表达错误的性质
- 增强错误处理的精确性:可以根据错误类型采取不同的处理策略
- 添加上下文信息:自定义属性可以提供更多关于错误的上下文
- 建立错误层次结构:通过继承关系组织不同类型的错误
- 实现特定的错误行为:可以为错误类型添加特定的方法
在设计自定义错误类型时,应遵循以下原则:
- 保持错误类型的一致性和可预测性
- 提供有意义的错误消息和上下文信息
- 确保错误对象包含足够的调试信息
- 考虑错误的可恢复性和重试策略
- 适当地组织错误类型的层次结构
通过合理使用自定义错误类型和错误处理策略,可以显著提高应用程序的健壮性和用户体验。
练习
创建一个
DatabaseError
类,包含数据库操作、SQL查询和错误代码等属性,并实现一个模拟数据库操作的函数来使用它。设计一个错误层次结构,包含
NetworkError
、HttpError
、ApiError
等类型,并实现一个函数来根据不同的错误类型显示不同的用户友好消息。创建一个
TimeoutError
类,并实现一个带有超时功能的函数,如果操作在指定时间内未完成,则抛出超时错误。实现一个错误处理中间件(对于Node.js应用),能够捕获并处理不同类型的错误,并返回适当的HTTP响应。
创建一个错误日志系统,能够记录不同类型的错误,包括时间戳、错误类型、消息和堆栈跟踪,并提供查询和过滤功能。
参考资源
- MDN Web Docs: Error
- MDN Web Docs: try...catch
- JavaScript Errors - Throw and Try to Catch
- Error Handling in Node.js