JavaScript 错误处理与调试完全指南

76 阅读4分钟

欢迎使用我的小程序👇👇👇👇👇

扫码_搜索联合传播样式-标准色版2.png

引言

在前几篇文章中,我们学习了JavaScript的基础、ES6特性、数组对象操作和异步编程。本文将深入探讨JavaScript开发中不可或缺的重要环节:错误处理与调试。这是成为优秀JavaScript开发者的关键技能。

一、JavaScript错误类型

1.1 内置错误类型

// SyntaxError: 语法错误
// let 1name = "John"; // 报错:变量名不能以数字开头

// ReferenceError: 引用错误
// console.log(undefinedVariable); // 报错:变量未定义

// TypeError: 类型错误
// const num = 123;
// num.toUpperCase(); // 报错:数字没有toUpperCase方法

// RangeError: 范围错误
// const arr = new Array(-1); // 报错:数组长度不能为负数

// URIError: URI相关错误
// decodeURIComponent('%'); // 报错:URI格式不正确

// EvalError: eval函数错误(现代JavaScript中较少见)

1.2 自定义错误类型

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
    this.timestamp = new Date().toISOString();
  }
}

class NetworkError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.name = "NetworkError";
    this.statusCode = statusCode;
  }
}

// 使用自定义错误
function validateUser(user) {
  if (!user.name) {
    throw new ValidationError("用户名不能为空", "name");
  }
  if (user.age < 0) {
    throw new ValidationError("年龄不能为负数", "age");
  }
}

二、错误处理机制

2.1 try...catch...finally

// 基本用法
function safeDivision(a, b) {
  try {
    console.log(`开始计算: ${a} ÷ ${b}`);
    if (b === 0) {
      throw new Error("除数不能为零");
    }
    const result = a / b;
    console.log(`计算结果: ${result}`);
    return result;
  } catch (error) {
    console.error("计算发生错误:", {
      message: error.message,
      name: error.name,
      stack: error.stack.split('\n').slice(0, 3).join('\n') // 只显示前3行调用栈
    });
    return null;
  } finally {
    console.log("计算过程结束,清理资源");
    // finally块总是会执行,无论是否发生错误
  }
}

safeDivision(10, 2);
safeDivision(10, 0);

2.2 异步错误处理

// Promise的错误处理
async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    
    if (!response.ok) {
      throw new NetworkError(
        `HTTP错误! 状态码: ${response.status}`,
        response.status
      );
    }
    
    const data = await response.json();
    return data;
  } catch (error) {
    if (error instanceof NetworkError) {
      console.error("网络请求错误:", error.message);
      // 重试逻辑
      return await retryFetch(userId);
    } else {
      console.error("未知错误:", error);
      throw error; // 重新抛出错误
    }
  }
}

// Promise链的错误处理
fetchUserData(123)
  .then(data => console.log("用户数据:", data))
  .catch(error => console.error("获取用户数据失败:", error))
  .finally(() => console.log("请求完成"));

// async/await中的错误处理优化
async function processMultipleRequests(ids) {
  const results = await Promise.allSettled(
    ids.map(id => fetchUserData(id).catch(error => ({
      error,
      id,
      status: 'failed'
    })))
  );
  
  const successes = results.filter(r => r.status === 'fulfilled');
  const failures = results.filter(r => r.status === 'rejected');
  
  console.log(`成功: ${successes.length}, 失败: ${failures.length}`);
  return { successes, failures };
}

三、浏览器调试工具详解

3.1 Chrome DevTools 高级调试技巧

// 调试示例函数
function complexCalculation(data) {
  // 1. 使用debugger语句设置断点
  // debugger;
  
  console.group("复杂计算开始");
  
  // 2. 条件断点(在DevTools中设置)
  // 在data.length > 100时中断
  const processed = data.map((item, index) => {
    // 3. 日志点(在DevTools中设置)
    const result = item.value * Math.sqrt(index);
    
    // 4. 监视表达式(在DevTools中设置)
    // index === 50
    // item.value > threshold
    
    return {
      ...item,
      result,
      normalized: result / 100
    };
  });
  
  console.table(processed.slice(0, 5)); // 显示前5条数据
  console.groupEnd();
  
  return processed;
}

// 性能调试
function performanceSensitiveFunction() {
  console.time("性能测试");
  
  // 使用Performance面板记录性能
  const results = [];
  for (let i = 0; i < 1000000; i++) {
    // 避免在循环中创建函数
    results.push(Math.sqrt(i) * Math.random());
  }
  
  console.timeEnd("性能测试");
  console.profileEnd(); // 与console.profile()配对使用
  
  return results;
}

3.2 Source Map与源代码调试

// 配置webpack source map
// webpack.config.js:
/*
module.exports = {
  devtool: 'source-map', // 开发环境推荐
  // devtool: 'cheap-module-source-map', // 生产环境推荐
  // ...
};
*/

// 使用eval进行动态代码调试(谨慎使用)
function dynamicCodeEvaluation(codeString) {
  try {
    // 使用严格模式的eval
    const result = (function() {
      "use strict";
      return eval(codeString);
    })();
    
    return {
      success: true,
      result
    };
  } catch (error) {
    console.error("动态代码执行错误:", {
      code: codeString,
      error: error.message,
      stack: error.stack
    });
    return {
      success: false,
      error: error.message
    };
  }
}

四、实用调试技巧与模式

4.1 错误追踪与日志记录

class Logger {
  constructor(context = "App") {
    this.context = context;
    this.levels = {
      DEBUG: 0,
      INFO: 1,
      WARN: 2,
      ERROR: 3
    };
    this.currentLevel = this.levels.DEBUG;
  }

  log(level, message, data = {}) {
    if (this.levels[level] >= this.currentLevel) {
      const timestamp = new Date().toISOString();
      const logEntry = {
        timestamp,
        level,
        context: this.context,
        message,
        data,
        userAgent: navigator?.userAgent || "Node.js"
      };

      // 控制台输出
      const colors = {
        DEBUG: 'color: gray',
        INFO: 'color: blue',
        WARN: 'color: orange',
        ERROR: 'color: red'
      };
      
      console.log(
        `%c[${timestamp}] ${level}: ${message}`,
        colors[level],
        data
      );

      // 发送到服务器(在生产环境中)
      if (level === 'ERROR' && process.env.NODE_ENV === 'production') {
        this.sendToServer(logEntry);
      }
    }
  }

  debug(message, data) {
    this.log('DEBUG', message, data);
  }

  error(message, error, extraData = {}) {
    const errorData = {
      message: error.message,
      name: error.name,
      stack: error.stack,
      ...extraData
    };
    this.log('ERROR', message, errorData);
  }

  async sendToServer(logEntry) {
    try {
      await fetch('/api/logs', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(logEntry)
      });
    } catch (e) {
      console.error('日志发送失败:', e);
    }
  }
}

// 使用示例
const logger = new Logger("UserService");

async function getUserProfile(userId) {
  logger.debug("获取用户资料", { userId });
  
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    logger.error("获取用户资料失败", error, { userId });
    throw error;
  }
}

4.2 防御性编程模式

// 1. 参数验证
function processOrder(order, options = {}) {
  // 参数类型检查
  if (!order || typeof order !== 'object') {
    throw new TypeError('order参数必须是对象');
  }

  // 必需字段检查
  const requiredFields = ['id', 'items', 'total'];
  for (const field of requiredFields) {
    if (!order.hasOwnProperty(field)) {
      throw new ValidationError(`订单缺少必需字段: ${field}`, field);
    }
  }

  // 默认值设置
  const config = {
    validate: true,
    logLevel: 'info',
    timeout: 5000,
    ...options
  };

  // 范围检查
  if (order.total < 0) {
    throw new RangeError('订单总金额不能为负数');
  }

  // 安全处理
  try {
    return validateAndProcess(order, config);
  } catch (error) {
    // 错误恢复策略
    if (config.fallback) {
      return config.fallback(order);
    }
    throw error;
  }
}

// 2. 数据验证函数
const Validators = {
  isEmail: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
  isPhone: (phone) => /^1[3-9]\d{9}$/.test(phone),
  isStrongPassword: (password) => password.length >= 8 && 
    /[A-Z]/.test(password) && 
    /[a-z]/.test(password) && 
    /\d/.test(password),
  
  validateObject: (obj, schema) => {
    const errors = [];
    
    for (const [key, validator] of Object.entries(schema)) {
      try {
        validator(obj[key]);
      } catch (error) {
        errors.push({
          field: key,
          error: error.message,
          value: obj[key]
        });
      }
    }
    
    return errors.length === 0 ? null : errors;
  }
};

// 3. 重试机制
async function withRetry(fn, options = {}) {
  const {
    maxAttempts = 3,
    delay = 1000,
    backoff = true,
    shouldRetry = () => true
  } = options;

  let lastError;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      
      if (attempt === maxAttempts || !shouldRetry(error)) {
        break;
      }
      
      const waitTime = backoff ? delay * Math.pow(2, attempt - 1) : delay;
      console.warn(`第${attempt}次尝试失败,${waitTime}ms后重试`, error.message);
      
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }
  }
  
  throw lastError;
}

// 使用示例
async function fetchWithRetry(url) {
  return await withRetry(
    () => fetch(url).then(r => {
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      return r.json();
    }),
    {
      maxAttempts: 5,
      delay: 1000,
      shouldRetry: (error) => !error.message.includes('404') // 404错误不重试
    }
  );
}

4.3 性能监控与错误上报

// 全局错误监听
window.addEventListener('error', (event) => {
  const { message, filename, lineno, colno, error } = event;
  
  const errorReport = {
    type: 'unhandledError',
    message,
    filename,
    position: `${lineno}:${colno}`,
    stack: error?.stack,
    timestamp: new Date().toISOString(),
    url: window.location.href,
    userAgent: navigator.userAgent
  };
  
  // 发送到错误监控服务
  sendErrorReport(errorReport);
  
  // 防止错误再次抛出(可选)
  event.preventDefault();
});

// Promise未捕获错误
window.addEventListener('unhandledrejection', (event) => {
  const errorReport = {
    type: 'unhandledRejection',
    reason: event.reason?.message || event.reason,
    stack: event.reason?.stack,
    timestamp: new Date().toISOString()
  };
  
  sendErrorReport(errorReport);
  event.preventDefault();
});

// 性能监控
function monitorPerformance() {
  // 使用Performance API
  if ('performance' in window) {
    const timing = performance.timing;
    
    const metrics = {
      dns: timing.domainLookupEnd - timing.domainLookupStart,
      tcp: timing.connectEnd - timing.connectStart,
      request: timing.responseStart - timing.requestStart,
      response: timing.responseEnd - timing.responseStart,
      domReady: timing.domContentLoadedEventEnd - timing.navigationStart,
      loadComplete: timing.loadEventEnd - timing.navigationStart,
      total: timing.loadEventEnd - timing.navigationStart
    };
    
    // 发送性能数据
    sendMetrics(metrics);
    
    // 长任务监控
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((list) => {
        list.getEntries().forEach(entry => {
          if (entry.duration > 50) { // 超过50ms的任务
            console.warn('长任务检测:', {
              name: entry.name,
              duration: entry.duration,
              startTime: entry.startTime
            });
          }
        });
      });
      
      observer.observe({ entryTypes: ['longtask'] });
    }
  }
}

// 内存泄漏检测
function checkMemoryLeaks() {
  if ('memory' in performance) {
    const { usedJSHeapSize, jsHeapSizeLimit } = performance.memory;
    const usagePercent = (usedJSHeapSize / jsHeapSizeLimit) * 100;
    
    if (usagePercent > 80) {
      console.warn('内存使用率过高:', `${usagePercent.toFixed(2)}%`);
      
      // 触发垃圾回收(仅在Chrome中有效)
      if (window.gc) {
        window.gc();
      }
    }
  }
}

// 定期检查
setInterval(checkMemoryLeaks, 60000); // 每分钟检查一次

五、实战案例:完整的错误处理系统

// 综合应用:一个完整的API请求模块
class ApiClient {
  constructor(baseURL, config = {}) {
    this.baseURL = baseURL;
    this.config = {
      timeout: 30000,
      retries: 3,
      logger: console,
      ...config
    };
    this.logger = this.config.logger;
    this.requestInterceptor = null;
    this.responseInterceptor = null;
  }

  // 请求拦截器
  useRequestInterceptor(interceptor) {
    this.requestInterceptor = interceptor;
  }

  // 响应拦截器
  useResponseInterceptor(interceptor) {
    this.responseInterceptor = interceptor;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const startTime = Date.now();
    
    const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2)}`;
    
    const requestOptions = {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'X-Request-ID': requestId,
        ...options.headers
      },
      timeout: this.config.timeout,
      ...options
    };

    // 请求拦截
    if (this.requestInterceptor) {
      const modifiedOptions = await this.requestInterceptor(requestOptions);
      Object.assign(requestOptions, modifiedOptions);
    }

    this.logger.debug('API请求开始', {
      requestId,
      url,
      method: requestOptions.method,
      timeout: requestOptions.timeout
    });

    let attempts = 0;
    let lastError;

    while (attempts <= this.config.retries) {
      attempts++;
      
      try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), requestOptions.timeout);
        
        requestOptions.signal = controller.signal;
        
        const response = await fetch(url, requestOptions);
        clearTimeout(timeoutId);

        const duration = Date.now() - startTime;
        
        // 响应拦截
        let processedResponse = response;
        if (this.responseInterceptor) {
          processedResponse = await this.responseInterceptor(response.clone());
        }

        if (!response.ok) {
          const error = new NetworkError(
            `HTTP ${response.status}: ${response.statusText}`,
            response.status
          );
          
          error.response = processedResponse;
          error.requestId = requestId;
          error.duration = duration;
          
          // 特定状态码不重试
          const noRetryStatuses = [400, 401, 403, 404];
          if (noRetryStatuses.includes(response.status)) {
            throw error;
          }
          
          lastError = error;
          continue;
        }

        let data;
        const contentType = response.headers.get('content-type');
        
        if (contentType?.includes('application/json')) {
          data = await response.json();
        } else {
          data = await response.text();
        }

        this.logger.info('API请求成功', {
          requestId,
          url,
          method: requestOptions.method,
          status: response.status,
          duration,
          dataSize: JSON.stringify(data).length
        });

        return {
          success: true,
          data,
          response: processedResponse,
          requestId,
          duration
        };

      } catch (error) {
        const duration = Date.now() - startTime;
        lastError = error;
        
        if (error.name === 'AbortError') {
          this.logger.error('请求超时', {
            requestId,
            url,
            method: requestOptions.method,
            duration
          });
        } else {
          this.logger.error('请求失败', {
            requestId,
            url,
            method: requestOptions.method,
            error: error.message,
            duration,
            attempt: attempts
          });
        }

        if (attempts > this.config.retries) {
          break;
        }

        // 指数退避延迟
        const delay = Math.min(1000 * Math.pow(2, attempts - 1), 10000);
        await this.sleep(delay);
      }
    }

    throw lastError;
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 快捷方法
  get(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  post(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data)
    });
  }

  put(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }

  delete(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'DELETE' });
  }
}

// 使用示例
const apiClient = new ApiClient('https://api.example.com', {
  timeout: 10000,
  retries: 3
});

// 添加拦截器
apiClient.useRequestInterceptor(async (options) => {
  const token = localStorage.getItem('auth_token');
  if (token) {
    options.headers.Authorization = `Bearer ${token}`;
  }
  return options;
});

apiClient.useResponseInterceptor(async (response) => {
  if (response.status === 401) {
    // 处理token过期
    localStorage.removeItem('auth_token');
    window.location.href = '/login';
  }
  return response;
});

// 使用API客户端
async function getProducts() {
  try {
    const result = await apiClient.get('/products');
    return result.data;
  } catch (error) {
    if (error instanceof NetworkError && error.statusCode === 404) {
      console.warn('产品API端点不存在');
      return [];
    }
    throw error;
  }
}

六、最佳实践总结

6.1 错误处理原则

  1. 尽早捕获,明确处理:在可能出错的地方添加错误处理
  2. 提供有意义的错误信息:错误消息应帮助开发者快速定位问题
  3. 不要静默忽略错误:至少记录错误,即使你决定不处理它
  4. 区分预期错误和意外错误:网络错误是预期的,语法错误是意外的
  5. 保持错误处理的层次性:在合适的层级处理错误

6.2 调试策略

  1. 从简单开始:先检查控制台,使用console.log
  2. 利用断点:条件断点、日志点、DOM断点
  3. 性能监控:关注内存泄漏和长任务
  4. 生产环境监控:实现错误收集和性能追踪
  5. 编写可调试的代码:清晰的命名、适当的注释、模块化设计

结语

掌握JavaScript错误处理和调试技巧是提升开发效率和代码质量的关键。通过本文的学习,你应该能够:

  1. 识别和处理各种JavaScript错误
  2. 熟练使用浏览器调试工具
  3. 实现健壮的错误处理机制
  4. 构建完整的错误监控系统
  5. 遵循错误处理的最佳实践

记住,优秀的开发者不是不犯错误,而是能够快速定位和修复错误。良好的错误处理习惯会让你的代码更加可靠,你的调试技巧会让你在解决问题时事半功倍。