【AI 编程实战】第 10 篇:让应用更健壮 - 错误处理与边界情况
功能做完了,但用户体验还差一步——当网络出错、接口超时、数据异常时,应用会不会崩溃?用户能不能看到友好的提示?这篇文章以心动恋聊小程序为例,展示如何和 AI 对话,构建健壮、友好、可维护的错误处理体系。
系列专栏:【AI 编程实战】专栏目录
本篇主题:让应用更健壮 - 错误处理与边界情况
实战项目:心动恋聊 - AI 恋爱聊天助手
一、开篇:错误处理的重要性
1.1 没有错误处理的代码
// ❌ 危险:没有任何错误处理
const handleGenerate = async () => {
const response = await chatService.generateReply({ text: inputText.value });
replies.value = response.data.replies;
};
可能出现的问题:
- 网络断开 → 应用白屏
- 接口超时 → 无任何反馈
- 返回数据为空 →
Cannot read property 'replies' of undefined - 用户点击多次 → 重复请求
1.2 完善的错误处理
// ✅ 健壮:完整的错误处理
const handleGenerate = async () => {
if (loading.value) return; // 防重复点击
loading.value = true;
try {
const response = await chatService.generateReply({ text: inputText.value });
if (response.code !== 200) {
throw new Error(response.msg || '生成失败');
}
replies.value = response.data?.replies ?? []; // 空值保护
} catch (error) {
const errMsg = error?.message || '网络错误,请稍后再试';
toast.error(errMsg);
console.error('生成回复失败:', error);
} finally {
loading.value = false;
}
};
1.3 本文要解决的问题
| 问题 | 解决方案 |
|---|---|
| 网络错误 | 统一错误提示 + 重试机制 |
| 接口错误码 | 分类处理(401/403/500) |
| 空值异常 | 可选链 + 空值合并 |
| 重复点击 | loading 状态锁定 |
| 用户取消 | 区分取消和真正的错误 |
二、统一错误提示:Toast 封装
2.1 我的需求
我:项目里很多地方要显示提示,有成功、失败、警告等类型。
每次都写 uni.showToast 太繁琐,能不能封装一下?
2.2 AI 设计方案
AI:封装一个 toast 工具,提供链式调用:
📐 接口设计:
toast.success('操作成功')
toast.error('操作失败')
toast.warning('请注意')
toast.info('提示信息')
📐 配置选项:
interface ToastOptions {
duration?: number; // 显示时长,默认 2000ms
position?: 'top' | 'middle' | 'bottom'; // 位置
mask?: boolean; // 是否显示遮罩防止穿透
}
2.3 AI 生成代码
// src/utils/toast.ts
type ToastType = 'success' | 'error' | 'warning' | 'info';
interface ToastOptions {
type?: ToastType;
duration?: number;
position?: 'top' | 'middle' | 'bottom';
message: string;
mask?: boolean;
}
export function showToast(message: string): void;
export function showToast(options: ToastOptions): void;
export function showToast(options: ToastOptions | string): void {
const defaultOptions: ToastOptions = {
type: 'info',
duration: 2000,
position: 'middle',
message: '',
mask: false,
};
const mergedOptions = typeof options === 'string'
? { ...defaultOptions, message: options }
: { ...defaultOptions, ...options };
// 映射 position 到 UniApp 支持的格式
const positionMap = {
top: 'top',
middle: 'center',
bottom: 'bottom',
};
uni.showToast({
title: mergedOptions.message,
duration: mergedOptions.duration,
position: positionMap[mergedOptions.position ?? 'middle'],
icon: 'none',
mask: mergedOptions.mask,
});
}
// 便捷方法
type SimpleToastOptions = Omit<ToastOptions, 'type' | 'message'>;
export const toast = {
success: (message: string, options?: SimpleToastOptions) =>
showToast({ ...options, type: 'success', message }),
error: (message: string, options?: SimpleToastOptions) =>
showToast({ ...options, type: 'error', message }),
warning: (message: string, options?: SimpleToastOptions) =>
showToast({ ...options, type: 'warning', message }),
info: (message: string, options?: SimpleToastOptions) =>
showToast({ ...options, type: 'info', message }),
};
2.4 使用示例
import { toast } from '@/utils/toast';
// 简单用法
toast.success('保存成功');
toast.error('网络错误,请稍后再试');
// 带配置
toast.info('正在处理...', { duration: 3000, mask: true });
三、接口错误分类处理
3.1 我的需求
我:后端返回不同的错误码:
- 401:未登录或 token 过期
- 403:没有权限(如 VIP 功能)
- 500:服务器错误
不同错误码要有不同的处理逻辑。
3.2 AI 设计错误分类
AI:在 HTTP 层统一处理常见错误码,业务层处理特殊错误:
📐 错误分类:
【HTTP 层处理】(在 http.ts 中)
- 401 → 清除登录态,跳转登录
- 网络错误 → 显示"网络错误"提示
【业务层处理】(在页面中)
- 403 → 引导开通 VIP
- 业务错误 → 显示具体错误信息
3.3 HTTP 层错误处理
// src/http/http.ts
export function http<T>(options: CustomRequestOptions) {
return new Promise<IResData<T>>((resolve, reject) => {
uni.request({
...options,
success(res) {
// 2xx 状态码视为成功
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data as IResData<T>);
}
// 401 未授权
else if (res.statusCode === 401) {
// 清除登录态
uni.removeStorageSync('token');
// 可以触发全局事件或跳转登录
reject(res);
}
// 其他错误
else {
if (!options.hideErrorToast) {
const data = res.data as IResData<T>;
uni.showToast({
icon: 'none',
title: data.msg || '请求错误',
});
}
reject(res);
}
},
// 网络错误
fail(err) {
uni.showToast({
icon: 'none',
title: '网络错误,换个网络试试',
});
reject(err);
},
});
});
}
3.4 业务层错误处理
// 页面中处理 403 等业务错误
const handleGenerate = async () => {
loading.value = true;
try {
const response = await chatService.generateReply(params);
if (response.code !== 200) {
throw new Error(response.msg || '生成失败');
}
replies.value = response.data?.replies ?? [];
} catch (error: any) {
console.error('生成回复失败:', error);
// 提取错误信息
const errMsg = error?.data?.message
|| error?.msg
|| error?.message
|| '生成失败,请稍后再试';
// 根据状态码分类处理
const statusCode = error?.statusCode || error?.data?.code || error?.code;
if (statusCode === 403) {
// 权限不足,引导开通 VIP
const message = errMsg || '免费次数已用完,请开通VIP';
promptOpenVip(message);
} else {
// 其他错误,显示提示
toast.error(errMsg);
}
} finally {
loading.value = false;
}
};
四、空值保护:防御性编程
4.1 常见的空值错误
// ❌ 危险:可能报错
const username = response.data.user.username;
// 如果 response.data 是 undefined,就会报错
const firstReply = replies[0].text;
// 如果 replies 是空数组,就会报错
4.2 AI 教我防御性编程
AI:用可选链(?.)和空值合并(??)来保护:
📐 防御性编程规则:
1. 访问对象属性 → 用 ?.
2. 提供默认值 → 用 ??
3. 数组访问 → 先检查长度或用 ?.[0]
4.3 实践示例
// ✅ 安全:可选链 + 空值合并
const username = response?.data?.user?.username ?? '未知用户';
// ✅ 安全:数组保护
const replies = response.data?.replies ?? [];
const firstReply = replies.length > 0 ? replies[0].text : '';
// ✅ 安全:解构时提供默认值
const { data = {} } = response ?? {};
const { replies = [], analysis = '' } = data;
// ✅ 安全:函数参数默认值
const updateUserInfo = (info: Partial<UserInfo> = {}) => {
// info 一定是对象,不会是 undefined
};
4.4 实际代码中的应用
// 生成回复的完整错误处理
const handleGenerate = async () => {
// ... 请求逻辑
const data = response.data ?? {};
// 安全提取分析结果
analysisResult.value = data.analysis || '';
// 安全提取回复列表,并添加 id
const repliesFromApi = Array.isArray(data.replies)
? data.replies.slice(0, MEMORY_REPLY_COUNT)
: [];
replies.value = repliesFromApi.map((r, idx) => ({
...r,
id: r.id ?? idx, // 如果没有 id,用索引代替
}));
// 安全更新配额信息
userStore.updateFreeQuota({
free_reply_total: data.freeReplyTotal,
free_reply_used: data.freeReplyUsed,
free_reply_remaining: data.remainingTimes,
vip_status: data.isVip ? 'vip' : 'none',
vip_expire_time: data.vipExpireTime ?? null,
});
};
五、用户取消 vs 真正的错误
5.1 问题场景
我:用户选择图片时点了"取消",会触发 fail 回调,
显示"图片选择失败"的错误提示。
但这不是真正的错误,不应该提示。
5.2 AI 分析区分方法
AI:通过错误信息来区分用户取消和真正的错误:
📊 错误信息分析:
【用户取消】
- chooseMedia:fail cancel
- chooseImage:fail cancel
- chooseImage:fail(code: 0)
【真正的错误】
- chooseImage:fail no permission
- chooseMedia:fail system error
- 其他错误信息
5.3 实现代码
const addImages = () => {
const { run } = useUpload('', {}, {
count: remainingCount,
selectOnly: true,
onSelect: (images) => {
// 正常处理选中的图片
images.forEach((img) => uploadChosenImage(img));
},
onError: (err) => {
// 判断是否为用户主动取消
const errMsg = typeof err === 'object' && err !== null && 'errMsg' in err
? (err as any).errMsg
: '';
const errCode = typeof err === 'object' && err !== null && 'code' in err
? (err as any).code
: -1;
const isUserCancel =
errMsg === 'chooseMedia:fail cancel' ||
errMsg === 'chooseImage:fail cancel' ||
(errMsg === 'chooseImage:fail' && errCode === 0);
if (!isUserCancel) {
// 只有真正的错误才提示
console.error('图片选择失败:', err);
toast.error('图片选择失败');
}
// 用户取消时静默处理,不显示任何提示
},
});
run();
};
六、Promise 封装:统一回调风格
6.1 我的需求
我:uni.login 等 API 用的是回调风格,写起来很繁琐。
能不能封装成 Promise 风格?
6.2 AI 封装示例
// src/utils/wechat.ts
/**
* 获取微信登录凭证
* 将回调风格封装成 Promise
*/
export const requestWechatLoginCode = () =>
new Promise<string>((resolve, reject) => {
uni.login({
provider: 'weixin',
onlyAuthorize: true,
success: (res) => {
if (res?.code) {
resolve(res.code);
} else {
reject(new Error('未获取到有效的登录凭证'));
}
},
fail: (err) => {
reject(err);
},
});
});
// 使用时
try {
const code = await requestWechatLoginCode();
await userStore.wechatLogin(code);
} catch (error) {
toast.error('登录失败');
}
6.3 剪贴板封装
// src/utils/clipboard.ts
import { isApp } from '@/utils/platform';
declare const plus: any;
/**
* 跨平台复制文本到剪贴板
*/
export function copyText(text: string): Promise<void> {
// App 端使用 plus API
if (isApp && typeof plus !== 'undefined' && plus?.navigator) {
return new Promise((resolve, reject) => {
try {
plus.navigator.setClipboard(text);
resolve();
} catch (error) {
reject(error);
}
});
}
// 小程序/H5 使用 uni API
return new Promise((resolve, reject) => {
uni.setClipboardData({
data: text,
success: () => resolve(),
fail: (err) => reject(err),
});
});
}
// 使用时
const handleCopy = async (text: string) => {
try {
await copyText(text);
toast.success('已复制');
} catch (error) {
console.error('复制失败:', error);
toast.error('复制失败,请稍后再试');
}
};
七、Loading 状态管理
7.1 防止重复点击
const loading = ref(false);
const handleSubmit = async () => {
// 防止重复点击
if (loading.value) return;
loading.value = true;
try {
await doSubmit();
} finally {
loading.value = false;
}
};
7.2 按钮状态联动
<template>
<XButton
:text="loading ? '处理中...' : '提交'"
:loading="loading"
:disabled="loading || !canSubmit"
@click="handleSubmit"
/>
</template>
7.3 多个 Loading 状态
// 页面有多个独立的加载状态
const isGenerating = ref(false); // 生成回复
const isUploadingImages = ref(false); // 上传图片
const isSaving = ref(false); // 保存数据
// 计算总体加载状态
const isLoading = computed(() =>
isGenerating.value || isUploadingImages.value || isSaving.value
);
// 细粒度控制按钮状态
<XButton
:disabled="isGenerating || isUploadingImages"
@click="handleGenerate"
/>
八、错误处理最佳实践
8.1 错误处理清单
| 层级 | 处理内容 | 示例 |
|---|---|---|
| HTTP 层 | 网络错误、401 | http.ts 统一处理 |
| 业务层 | 403、业务错误码 | 页面 catch 块处理 |
| UI 层 | 用户提示、状态恢复 | toast + loading |
8.2 代码模板
// 标准的异步操作模板
const handleAsyncAction = async () => {
// 1. 防重复
if (loading.value) return;
// 2. 前置校验
if (!canSubmit.value) {
toast.warning('请填写完整信息');
return;
}
loading.value = true;
try {
// 3. 执行操作
const response = await doAction();
// 4. 校验响应
if (response.code !== 200) {
throw new Error(response.msg || '操作失败');
}
// 5. 处理成功
const data = response.data ?? {};
processData(data);
toast.success('操作成功');
} catch (error: any) {
// 6. 分类处理错误
console.error('操作失败:', error);
const statusCode = error?.statusCode || error?.code;
const errMsg = error?.msg || error?.message || '操作失败';
if (statusCode === 403) {
handleNoPermission();
} else {
toast.error(errMsg);
}
} finally {
// 7. 恢复状态
loading.value = false;
}
};
8.3 总结
| 技术 | 用途 | 示例 |
|---|---|---|
| try-catch-finally | 捕获和恢复 | 异步操作的标准模式 |
可选链 ?.
|
安全访问属性 | response?.data?.user |
空值合并 ??
|
提供默认值 | value ?? defaultValue |
| Promise 封装 | 统一回调风格 | requestWechatLoginCode() |
| 错误分类 | 差异化处理 | 401/403/500 不同处理 |
| Loading 状态 | 防重复 + 用户反馈 | if (loading) return |
错误处理不是"事后补救",而是设计阶段就要考虑的架构问题。
好的错误处理让用户感受到应用的专业和可靠。
如果这篇文章对你有帮助,请点赞、收藏、转发!