一文掌握dart中的错误处理

202 阅读4分钟

前言

本文章受众需要掌握一定dart语言开发能力。

本文不对dart的错误处理Error handling/并发机制Asynchrony support做介绍。当在对dart的同步/异步机制有一定学习认识之后,这个文章会帮助你避开实践过程中容易踩的坑,真正掌握dart异常处理机制。

正文采用分-总结构,以提出问题-分析实践-得出结论的层次阐述。

如果想直接看结论,可以跳到最后一节“总结”。

测试代码

void main() async {
  // await workerManager.init();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}


class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  
  @override
  void initState() {
    super.initState();
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
      /// 多层级捕获
      tryErrorTopLevelCatch();
    });
  }
	/// 顶层捕获
  void tryErrorTopLevelCatch() {
    try {
      middleCatchError();
    } catch (e) {
      log('tryErrorTopLevelCatch: $e');
    }
  }

  /// 中间层捕获
  void middleCatchError() async {
    try {
      await asyncThrowError();
      // throwError();
    } catch (e) {
      log('middleCatchError: $e');
    }
  }

  /// 异步函数
  Future<void> asyncThrowError() async {
    throw 'throw err';
  }

  /// 同步函数
  void throwError() {
    throw 'throw err';
  }
}

void log(String str) {
  // 获取当前时间
  var now = DateTime.now();
  print('[$now] $str');
}

重点在_incrementCounter方法中

void _incrementCounter() {
    setState(() {
      _counter++;
      tryErrorTopLevelCatch();
    });
  }

通过点击悬浮+按钮,调用tryErrorTopLevelCatch(),过程中,我们不断调整middleCatchError,asyncThrowError,throwError的方法实现,才达到我们测试的目的并得出结论。

错误场景

1. dart中try-catch是否支持捕获多层级调用栈异常?

改动如下

/// 顶层捕获
  void tryErrorTopLevelCatch() {
    try {
      middleCatchError();
    } catch (e) {
      log('tryErrorTopLevelCatch: $e');
    }
  }

  /// 中间层捕获
  void middleCatchError() {
    throwError();
  }

  /// 同步函数
  void throwError() {
    throw 'throw err';
  }

输出:tryErrorTopLevelCatch: throw err

结论:异常未在当前函数域捕获时,会一层层抛到上层调用栈。通过try-catch可以捕获处理异常

2. 并发场景下,try-catch是否能捕获处理异常

修改代码如下


  /// 中间层捕获,增加了async标识该函数为异步执行
  Future<void> middleCatchError() async {
    throwError();
  }

结果:程序抛出异常

将tryErrorTopLevelCatch实现改动如下

void tryErrorTopLevelCatch() async {
    try {
      await middleCatchError();
    } catch (e) {
      log('tryErrorTopLevelCatch: $e');
    }
  }

输出:tryErrorTopLevelCatch: throw err

结论:try catch机制只能捕获同步函数执行异常,对于异步函数,需要增加await关键字,将函数调用方式更改为等同同步执行,才能正确捕获。

那么,如果不增加await,采用Future的catchError机制能否正确捕获异常呢?

将顶层tryErrorTopLevelCatch实现改动如下

/// 顶层捕获
  void tryErrorTopLevelCatch() {
   middleCatchError().catchError((err) {
      log('async tryErrorTopLevelCatch: $e');
    });
  }

结果:程序抛出异常

将throwError改成异步执行。

Future<void> middleCatchError() async {
  asyncThrowError();
}

/// 异步函数
  Future<void> asyncThrowError() async {
    throw 'throw err';
  }

结果:程序抛出异常

中间异步函数增加await,是否能捕获?

Future<void> middleCatchError() async {
  await asyncThrowError();
}

结果:程序抛出异常

只有在当前发生异常函数的调用处使用catchError捕获异常时才能成功

Future<void> middleCatchError() {
    asyncThrowError().catchError((e) {
      log('catchError: $e');
    });

输出:catchError: throw err

结论:Future的catchError无法处理跨栈异常,无论是同步的还是异步的。捕获跨栈的异常,统一采用try-catch,需要注意的是针对异步函数需使用await转成同步语法。

3. catchError和onError同时存在时,会由哪个优先处理

改动如下

void middleCatchError() async {
  asyncThrowError()
    .onError((error, stackTrace) => log('onError: $error'))
    .catchError((e) => log('catchError: $e'));
}

Future<void> asyncThrowError() async {
  throw 'throw err';
}

输出:onError: throw err

变更下顺序

void middleCatchError() async {
  asyncThrowError()
        .catchError((e) => log('catchError: $e'))
        .onError((error, stackTrace) => log('onError: $error'));
}

Future<void> asyncThrowError() async {
  throw 'throw err';
}

输出:catchError: throw err

结论:可知,catchError和onError同时存在时,谁紧跟在调用方法后,谁处理。

4. try-catch和Future的onError/catchError同时存在时,由谁处理异常

void middleCatchError() async {
  try {
    asyncThrowError()
    .onError((error, stackTrace) => log('onError: $error'))
    .catchError((e) => log('catchError: $e'));
  } catch (e) {
    log('middleCatchError: $e');
  }
}

Future<void> asyncThrowError() async {
  throw 'throw err';
}

输出:onError: throw err

同时使用await修饰时

void middleCatchError() async {
  try {
    await asyncThrowError()
    .onError((error, stackTrace) => log('onError: $error'))
    .catchError((e) {
    log('catchError: $e');
    };
  } catch (e) {
    log('middleCatchError: $e');
  }
}

Future<void> asyncThrowError() async {
  throw 'throw err';
}

输出:onError: throw err

当没有实现Future的异常处理块onError/catchError时

void middleCatchError() async {
  try {
    await asyncThrowError();
    };
  } catch (e) {
    log('middleCatchError: $e');
  }
}

Future<void> asyncThrowError() async {
  throw 'throw err';
}

输出:middleCatchError: throw err

结论:使用await可以捕获异步函数同步执行抛出的异常。try-catch无论有无await修饰,当和catchError/onError同时存在时,catchError/onError优先处理异常

5. 异步函数中,执行了耗时任务时,耗时任务发生异常,能否正确被上层捕获。

修改如下

Future<void> asyncThrowError() async {
    // throw 'throw err';
    Future.delayed(const Duration(seconds: 1), () {
      throw 'throw err';
    });
  }

结果:异常未正确捕获。

修改成

Future<void> asyncThrowError() async {
    Future.delayed(const Duration(seconds: 1), () {
      throw 'throw err';
    }).catchError((e) {
      log('throwError: $e');
      throw e;
    });
  }

输出:throwError:throw err

之后程序异常中断。

分析: 1.能正确输出,说明耗时任务抛出的异常被处理了。在这个场景中,异步任务中执行耗时任务,耗时任务抛出异常时,其实耗时任务块可以拆出写成函数形式,那么其实该问题跟上面问题2一样了,即对于当前调用的异步函数,需要对会执行异常的耗时任务增加catchError/onError块处理才行。

2.最后程序异常中断,说明catchError/onError块中抛出的异常,无法被上层try-catch或者Future的onError/catchError异常处理捕获到。原因目前未知。所以我们日常开发中要确保catchError/onError中不会执行抛出异常。

总结

1.同步函数的执行异常,可以通过try-catch跨多层捕获,前提是中间层函数都是同步调用的。

2.异步函数的执行异常,可以被当前执行函数上级调用栈通过Future的catchError/onError捕获处理。也可以将该函数通过await改成同步调用,这时可以认为变成了总结1中的同步函数执行情景,可以通过try-catch块进行跨多层调用栈捕获

3.try-catch无论有无await修饰,当和catchError/onError同时存在时,catchError/onError优先处理异常。而catchError和onError同时存在时,异常由catchError还是onError处理,只跟调用顺序有关,谁紧跟在调用方法后,谁处理。