From 596e38ff48ac7c29ff7d167d81d2d0d38da162cc Mon Sep 17 00:00:00 2001 From: Meylis Annagurbanov Date: Sat, 9 May 2026 22:53:09 +0500 Subject: [PATCH] fix(bloc): omit synthetic `BlocBase.addError` frame from auto-captured stack traces --- packages/bloc/CHANGELOG.md | 4 +++ packages/bloc/lib/src/bloc_base.dart | 13 +++++++++- packages/bloc/pubspec.yaml | 2 +- packages/bloc/test/cubit_test.dart | 37 ++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/bloc/CHANGELOG.md b/packages/bloc/CHANGELOG.md index 9286866507c..23b311c546b 100644 --- a/packages/bloc/CHANGELOG.md +++ b/packages/bloc/CHANGELOG.md @@ -1,3 +1,7 @@ +# 9.3.0 + +- fix: omit synthetic `BlocBase.addError` frame from stack traces auto-captured when no `stackTrace` is passed to `addError` ([#3585](https://github.com/felangel/bloc/issues/3585)) + # 9.2.0 - feat: add `MultiBlocObserver` ([#4714](https://github.com/felangel/bloc/pull/4714)) diff --git a/packages/bloc/lib/src/bloc_base.dart b/packages/bloc/lib/src/bloc_base.dart index 51e374ce500..206e3a8703e 100644 --- a/packages/bloc/lib/src/bloc_base.dart +++ b/packages/bloc/lib/src/bloc_base.dart @@ -138,11 +138,15 @@ abstract class BlocBase } /// Reports an [error] which triggers [onError] with an optional [StackTrace]. + /// + /// If no [stackTrace] is provided, the current stack trace is captured with + /// the synthetic `BlocBase.addError` frame stripped, so the resulting trace + /// begins at the caller's call site. @protected @mustCallSuper @override void addError(Object error, [StackTrace? stackTrace]) { - onError(error, stackTrace ?? StackTrace.current); + onError(error, stackTrace ?? _withoutTopFrame(StackTrace.current)); } /// Called whenever an [error] occurs and notifies [BlocObserver.onError]. @@ -175,4 +179,11 @@ abstract class BlocBase _blocObserver.onClose(this); await _stateController.close(); } + + static StackTrace _withoutTopFrame(StackTrace trace) { + final raw = trace.toString(); + final newline = raw.indexOf('\n'); + if (newline < 0) return trace; + return StackTrace.fromString(raw.substring(newline + 1)); + } } diff --git a/packages/bloc/pubspec.yaml b/packages/bloc/pubspec.yaml index c204fa80f88..9ba4dd48778 100644 --- a/packages/bloc/pubspec.yaml +++ b/packages/bloc/pubspec.yaml @@ -1,6 +1,6 @@ name: bloc description: A predictable state management library that helps implement the BLoC (Business Logic Component) design pattern. -version: 9.2.0 +version: 9.3.0 repository: https://github.com/felangel/bloc/tree/master/packages/bloc issue_tracker: https://github.com/felangel/bloc/issues homepage: https://github.com/felangel/bloc diff --git a/packages/bloc/test/cubit_test.dart b/packages/bloc/test/cubit_test.dart index 6d5360cb57b..d5943ea64e3 100644 --- a/packages/bloc/test/cubit_test.dart +++ b/packages/bloc/test/cubit_test.dart @@ -66,6 +66,43 @@ void main() { () => observer.onError(cubit, expectedError, expectedStackTrace), ).called(1); }); + + test('omits BlocBase.addError frame when no stackTrace is passed', + () async { + StackTrace? captured; + CounterCubit( + onErrorCallback: (_, stackTrace) => captured = stackTrace, + // ignore: invalid_use_of_protected_member + ).addError(Exception('fatal exception')); + + expect(captured, isNotNull); + final firstLine = captured!.toString().split('\n').first; + expect(firstLine, isNot(contains('BlocBase.addError'))); + expect(firstLine, isNot(contains('bloc_base.dart'))); + }); + + test('passes through an explicit stackTrace unchanged', () async { + final explicit = StackTrace.fromString('#0 user_call (test.dart:1)'); + StackTrace? captured; + CounterCubit( + onErrorCallback: (_, stackTrace) => captured = stackTrace, + // ignore: invalid_use_of_protected_member + ).addError(Exception('fatal exception'), explicit); + + expect(captured, same(explicit)); + }); + + test('captured stackTrace is non-empty when no stackTrace is passed', + () async { + StackTrace? captured; + CounterCubit( + onErrorCallback: (_, stackTrace) => captured = stackTrace, + // ignore: invalid_use_of_protected_member + ).addError(Exception('fatal exception')); + + expect(captured, isNotNull); + expect(captured!.toString(), isNotEmpty); + }); }); group('onChange', () {