error

186 阅读10分钟

来看一段代码:

let ps = new Promise((resolve) => {
  setTimeout(() => {
    console.log('ps');
    resolve('1');
  }, 2000);
})

ps.then(res => {
  console.log('res', res);
  throw Error('fail');
}).catch(err => {
  console.log('err1', err);
}).catch(err => {
  console.log('err2', err);
})

这段代码的输出是:

image.png

可以看出,当前面catch已经成功处理后,后面的catch并未执行。这也意味着上层如果调用这个代码也并不会执行catch。

这样的设计是为了什么呢?

Promise链中的错误处理机制如下:

  • 当Promise链中的某个环节抛出错误时,链中下一个catch会捕获到这个错误。
  • 一旦错误被某个catch处理了,后续的catch将不会再处理这个错误,除非在第一个catch内部再次抛出错误。

这种设计是为了让错误处理更加明确和灵活。每个catch只处理它之前的错误,如果已经处理了错误,链上的其他部分可以认为错误已经得到处理,无需重复处理。这使得代码更加简洁,错误处理逻辑更加清晰。

具体到这段代码,设计的意图可以总结为:

  • 清晰的错误处理:每个catch只处理一个错误,避免多次处理同一个错误导致的混乱。
  • 链式调用:允许在一个Promise链中连续进行操作,每个操作的错误都可以独立处理。

这样,当你在开发中使用Promise时,可以更好地组织和管理异步代码,并明确知道每个错误会在哪里被捕获和处理。

我的思考

清晰的错误处理,这个的意思应该是各部分的代码职责明确。

在错误出现的地方,需要知道错误在这层该不该处理(是否是这层的职责,是否错误是这层最了解),然后问能不能处理,处理分为中止运行、抛出异常处理、容错。

该不该处理是为了职责清晰(可以根据信息内容是否在这层的范围内来判断),在对应地方才能恰当处理,这样整个系统的错误才清晰。一般在后端认为严重错误需要终止程序或向上抛出异常(抛出异常前也可以处理一点事情),表示下层履行职责出现问题需要上层处理。而如果错误可以在下层容错的话,这样在上层看来表示下层履行了职责并且错误得到合适处理,视为下层是符合预期的职责的。

能不能处理好像是废话,出现了错误就是不能处理了,只是在做补偿措施(容错),经过补偿后在上层看来这个还是正确履行职责了。

但是前端很少说是使用终止程序或抛出异常的,尽管 throw Error 在某些情况下是必须的,比如明确抛出并捕获错误、实现严格的错误处理逻辑,但在前端开发中,console.error() 提供了一种更为温和的错误处理方式,不会中断程序执行,同时允许开发人员记录错误信息并继续处理其他逻辑,从而提高了应用的健壮性和用户体验。

console.error

再来看这段代码:

let ps = new Promise((resolve) => {
  setTimeout(() => {
    console.log('ps');
    resolve('1');
  }, 2000);
})

ps.then(res => {
  console.log('res', res);
  throw Error('err0');
}).catch(err => {
  console.error('err1', err);
  throw Error('err1');
}).catch(err => {
  console.error('err2', err);
  throw Error('err2');
})

console.log('running');

异步代码里面的报错是不影响外边同步代码的执行的。使用 console.error()throw Error 的目的是为了在处理和记录错误的同时,允许进一步抛出错误以便更上层的错误处理逻辑来处理。例如:

  1. console.error() : 用于记录错误信息,这样即使错误被捕获并处理了,开发者仍然可以在控制台看到错误详情,便于调试。

  2. throw Error:在 catch 中重新抛出一个新的错误,允许错误继续传播到下一个错误处理程序。这可以帮助创建更复杂和详细的错误处理逻辑。

但是已经console.error了,在更高层次如果也console.error不会造成混乱吗?

确实,重复使用 console.error 可能会导致日志中出现重复的错误信息,特别是在错误被多个层级处理的情况下。不过,在实际开发中,这种方式通常是为了确保错误信息不会被忽略,同时在不同层级提供不同的上下文或处理措施。

为了避免混乱,开发者可以采用一些最佳实践:

1. 添加上下文信息

在不同层级的 catch 块中,添加特定的上下文信息,使得每个错误日志条目都包含其独特的背景。

javascript
复制代码
ps.then(res => {
  console.log('res', res);
  throw new Error('err1');
}).catch(err => {
  console.error('Caught in first catch:', err);
  throw new Error('err2');
}).catch(err => {
  console.error('Caught in second catch:', err);
});

这样,日志输出会明确指示错误是在哪一层级被捕获的:

vbnet
复制代码
running
ps
res 1
Caught in first catch: Error: err1
Caught in second catch: Error: err2

2. 统一的错误处理函数

将错误处理逻辑抽象成一个统一的函数,减少重复代码,并确保一致的错误处理方式。

javascript
复制代码
function handleError(stage, err) {
  console.error(`${stage}:`, err);
  // 这里可以添加其他的错误处理逻辑,如发送错误到远程服务器
}

ps.then(res => {
  console.log('res', res);
  throw new Error('err1');
}).catch(err => {
  handleError('First catch', err);
  throw new Error('err2');
}).catch(err => {
  handleError('Second catch', err);
});

3. 区分致命错误和非致命错误

有时候可以通过区分致命错误(需要抛出并传播的错误)和非致命错误(仅记录日志即可的错误)来减少混乱。

javascript
复制代码
ps.then(res => {
  console.log('res', res);
  throw new Error('err1');
}).catch(err => {
  if (isCriticalError(err)) {
    console.error('Critical error:', err);
    throw new Error('err2');
  } else {
    console.warn('Non-critical error:', err);
  }
}).catch(err => {
  console.error('Caught in second catch:', err);
});

4. 使用不同的日志级别

通过使用不同的日志级别(如 console.warn, console.info)来区分不同重要性的错误或信息。

javascript
复制代码
ps.then(res => {
  console.log('res', res);
  throw new Error('err1');
}).catch(err => {
  console.error('Critical error in first catch:', err);
  throw new Error('err2');
}).catch(err => {
  console.error('Critical error in second catch:', err);
});

console.log('running');

总结

虽然重复使用 console.error 可能会导致日志中的信息冗余,但通过适当的上下文信息、统一的错误处理函数和不同的日志级别,可以有效地管理和组织错误日志,使其更加清晰和有用。这样做不仅有助于在开发和调试过程中追踪和修复错误,还可以确保在生产环境中有足够的信息来分析和处理问题。

应用场景

这种模式在实际开发中非常有用。例如:

  • 当你希望记录一个错误,但仍然需要将错误传递给更高层次的处理程序。
  • 当你在某个层级捕获到错误,并想转换成另一种类型的错误或添加更多上下文信息,然后再抛出。
  • 在多个 catch 中处理不同类型的错误,或在不同层级中采取不同的错误处理策略。

这可以提高代码的可维护性和可读性,同时确保错误不会被静默丢失,而是被适当记录和处理。

ts的作用

函数本身只关心自己的职责和入参出参的设计,上层对调用的函数关心的是入参出参。入参出参属于数据结构,通过ts可以对数据结构在编译阶段进行内部约束(意思是用户是程序员),但不是用在在运行时对用户的输入约束。 如果不使用ts,那么就没有了全面的编译阶段检查机制,需要一种轻量的对公开的接口进行检查的处理和完整的jsdocs,因为这些需要做检查减少数据结构的错误和错误蔓延,对于内部的函数则可以适量减少检查,因为上下文已经有了入参和出参的约束。

为什么很少看到前端应用系统规范用console的日志功能

前端应用系统规范使用 console 的日志功能相对较少,主要有以下几个原因:

1. 日志持久性

  • 浏览器会话限制: console 日志通常只存在于浏览器的当前会话中,页面刷新或关闭后日志信息会丢失。这对于调试可能足够,但对于长期监控和分析不足。
  • 无法保存和共享: 开发者无法轻松地将 console 日志保存、共享或分析,限制了日志在协作和远程调试中的作用。

2. 性能问题

  • 大量日志影响性能: 大量使用 console.log 等日志功能可能会影响浏览器性能,尤其是在生产环境中频繁记录日志时。
  • 阻塞执行: 尽管现代浏览器优化了 console 的性能,但过多的日志输出仍可能在某些情况下阻塞 JavaScript 的执行。

3. 日志管理功能有限

  • 缺乏结构化日志: console 日志通常是非结构化的文本,难以进行系统化的分析和处理。
  • 缺乏日志等级: 虽然有 console.error, console.warn, console.info 等,但没有更高级的日志等级管理和过滤功能。

4. 安全性

  • 泄露敏感信息: 在生产环境中,日志中可能包含敏感信息,使用 console 日志可能导致这些信息暴露在浏览器控制台中,存在安全风险。
  • 调试信息暴露: 攻击者可能通过查看控制台日志获取有关应用程序的内部信息,增加攻击风险。

5. 专业日志工具的优势

现代前端开发通常依赖于专业的日志记录和管理工具,这些工具提供了更强大的功能和更好的集成:

  • 集中管理: 专业日志工具(如 Sentry、LogRocket、DataDog 等)可以集中管理日志,提供实时监控、告警和历史记录查询功能。
  • 结构化和可查询: 这些工具支持结构化日志记录,提供强大的查询和分析能力,帮助开发者快速定位和解决问题。
  • 持久化和可视化: 日志信息可以持久化存储,并提供可视化分析功能,帮助更好地理解应用运行状态和性能。
  • 集成和自动化: 可以与 CI/CD 流水线和其他开发工具集成,实现自动化日志管理和监控。

6. 最佳实践

以下是一些前端日志管理的最佳实践:

  • 开发和生产环境分离: 在开发环境中可以使用 console 日志进行调试,但在生产环境中应使用专业的日志管理工具。
  • 适度使用 console: 保持 console 日志的适度使用,避免在生产环境中大量输出日志。
  • 使用环境变量: 使用环境变量控制日志的启用和级别,确保在生产环境中只记录必要的日志信息。
  • 敏感信息保护: 避免在日志中记录敏感信息,确保用户数据安全。

示例:使用专业日志工具

以下是使用 Sentry 记录错误日志的示例:

javascript
复制代码
import * as Sentry from '@sentry/browser';

Sentry.init({ dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' });

try {
  // some code that might throw an error
} catch (error) {
  Sentry.captureException(error);
  console.error('Error captured and sent to Sentry:', error);
}

这种方式确保了错误日志的持久化和集中管理,同时在控制台中记录错误信息供开发者调试。