Skip to content

Fix: Ensure Image.errorBuilder reliably prevents error reporting#166130

Closed
PerLycke wants to merge 3 commits into
flutter:masterfrom
PerLycke:fix-image-error-handling-during-transitions
Closed

Fix: Ensure Image.errorBuilder reliably prevents error reporting#166130
PerLycke wants to merge 3 commits into
flutter:masterfrom
PerLycke:fix-image-error-handling-during-transitions

Conversation

@PerLycke

@PerLycke PerLycke commented Mar 28, 2025

Copy link
Copy Markdown

Problem:

Currently, when using an Image widget with an errorBuilder, if the widget is removed from the widget tree (e.g., due to navigation or setState) after the image loading process has started but before an asynchronous loading error is reported back, the error can still be reported via FlutterError.reportError. This occurs because the _ImageState listener is removed upon disposal, and the ImageStreamCompleter subsequently treats the error as unhandled, logging it despite the developer's intent to handle it via the errorBuilder. This leads to unexpected noise in logs and crash reporting systems.

Solution:

This PR implements a coordinated two-part fix in widgets/image.dart and painting/image_stream.dart:

  1. Modified _ImageState._getListener (widgets/image.dart):

    • The internal onError callback attached to the ImageStreamListener is now always non-null.
    • This listener always calls setState({... _lastException = error ...}) upon receiving an error. This ensures the state has the error information needed to render appropriate visual feedback.
    • This listener now calls FlutterError.reportError directly only if widget.errorBuilder == null. This ensures errors are reported when no user handler is provided.
    • The listener no longer throws exceptions itself.
  2. Modified ImageStreamCompleter (painting/image_stream.dart):

    • A new boolean flag, _hadErrorHandlerListener, is added.
    • In addListener, this flag is set to true if the added listener has a non-null onError callback.
    • The final check in reportError that previously called FlutterError.reportError if !handled is now changed to if (!handled && !_hadErrorHandlerListener).

How it works:

This approach ensures the primary goal (respecting errorBuilder even on disposal) is met while also preserving the correct visual error feedback in debug mode when no errorBuilder is present (because setState is always called upon error).

  • When errorBuilder is present, the completer's final report is suppressed if the widget is disposed, respecting the handler.
  • When errorBuilder is absent, the _ImageState listener reports the error and updates state, ensuring the error is logged and the debug visual is rendered as before.

Related Issues:

Tests:

  • Added a new test case errorBuilder prevents FlutterError report even if widget is disposed to test/widgets/image_test.dart to specifically verify the fix for the disposal race condition.
  • Existing tests in test/widgets/image_test.dart (including golden tests like 'Failed image loads in debug mode') pass with these changes without requiring updates.

Breaking Changes:

  • None. This change fixes incorrect behavior and preserves expected debug visuals. The internal mechanism for reporting errors when no errorBuilder is present has shifted, but the user-facing outcome is consistent.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@github-actions github-actions Bot added the framework flutter/packages/flutter repository. See also f: labels. label Mar 28, 2025
@PerLycke PerLycke force-pushed the fix-image-error-handling-during-transitions branch from 4cbc3c9 to 40eb238 Compare March 31, 2025 12:15
@PerLycke PerLycke marked this pull request as draft April 1, 2025 07:58
@PerLycke PerLycke force-pushed the fix-image-error-handling-during-transitions branch from 40eb238 to f1b49a3 Compare April 1, 2025 11:50
@PerLycke PerLycke changed the title Fix: Image errorBuilder handles errors during navigation transitions Fix: Image errorBuilder handles errors when TickerMode is disabled Apr 1, 2025
@PerLycke

PerLycke commented Apr 1, 2025

Copy link
Copy Markdown
Author

Updating this PR with a revised approach. Instead of using an ephemeral listener, this fix now conditionally keeps the primary listener attached when TickerMode is disabled and an errorBuilder is present. This is simpler, covers the necessary scenarios, and integrates better with the existing _ImageState error handling. The description and commit have been updated accordingly.

@PerLycke PerLycke marked this pull request as ready for review April 1, 2025 12:44
@flutter-dashboard

Copy link
Copy Markdown

Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change).

If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #166130 at sha f1b49a3

@flutter-dashboard flutter-dashboard Bot added the will affect goldens Changes to golden files label Apr 1, 2025
@PerLycke PerLycke marked this pull request as draft April 1, 2025 13:58
@flutter-dashboard

Copy link
Copy Markdown

This pull request has been changed to a draft. The currently pending flutter-gold status will not be able to resolve until a new commit is pushed or the change is marked ready for review again.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@PerLycke PerLycke force-pushed the fix-image-error-handling-during-transitions branch from f1b49a3 to e17fb63 Compare April 2, 2025 12:54
@PerLycke PerLycke changed the title Fix: Image errorBuilder handles errors when TickerMode is disabled Fix: Ensure Image.errorBuilder reliably prevents error reporting Apr 2, 2025
When an Image widget included an errorBuilder, errors during image
loading could still be reported via FlutterError.reportError if the
widget was disposed before the asynchronous error completed. This occurred
because the disposed widget's listener was removed from the
ImageStreamCompleter, causing the completer to incorrectly treat the
error as unhandled.

This change implements a two-part fix:

1.  ImageStreamCompleter now tracks if a listener with an `onError`
    callback was ever attached (`_hadErrorHandlerListener`). Its final
    error reporting logic skips the report if this flag is true and the
    error wasn't handled by a currently attached listener, correctly
    handling the disposal race condition.

2.  _ImageState's listener now always attaches a non-null `onError`
    callback. This callback always updates the state (`_lastException`)
    to enable visual error feedback. It only calls FlutterError.reportError
    directly if `errorBuilder` was null, shifting the reporting
    responsibility for that case from the completer to the listener.

This ensures errors are correctly handled or suppressed according to the
presence of an `errorBuilder`, even across widget disposal, and preserves
the expected visual debug error placeholder for failed images when no
`errorBuilder` is present.

Added test: 'errorBuilder prevents FlutterError report even if widget is disposed'.

Fixes flutter#97077
@PerLycke PerLycke force-pushed the fix-image-error-handling-during-transitions branch from e17fb63 to 4af29fc Compare April 2, 2025 13:05
@PerLycke

PerLycke commented Apr 3, 2025

Copy link
Copy Markdown
Author

Had to go a couple of rounds with this PR to find a comprehensive solution that reliably and properly fixes the problem. This new approach addresses a general disposal race condition more robustly. It also ensures correct debug visual feedback is preserved.

Ready for review.

@PerLycke PerLycke marked this pull request as ready for review April 3, 2025 07:21
@dkwingsmt dkwingsmt self-requested a review April 8, 2025 22:23
@PerLycke

Copy link
Copy Markdown
Author

We've been running this PR in production for a couple of weeks now and it successfully does what it claims, no more unwanted reports in Crashlytics, and no side effects. Question to the reviewer, do you see a better, or more proper, way of fixing the issue? Do you want me to refactor on your recent changes to image_stream, or what's the plan?

@dkwingsmt dkwingsmt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt if this is the right approach.

To analyze this issue: the core problem is that Image's disposal will always want to dispose the image stream and remove all its listeners, at which point the error has to be reported due to having no listeners. This is an issue of the model, for which I can think of a few approaches. In general we can gracifully mark the stream "ended" to prevent its error reporting. On the other hand, the current PR kind of marks the image completer "forever tainted" by ever having one error reporter, which feels unnecessarily hacky.

Although the image stream is not built with a disposing method for now (which we can add one if necessary), I found another "workaround": addEphemeralErrorListener allows the first error to be handled without preventing the disposal. Do you think it's a good idea to instead make Image add an ephemeral error listener if the image widget has an onError handler?

Comment on lines +511 to +514
/// Tracks if any listener with an onError callback was ever added.
/// Used to prevent reporting errors even if the listener is removed
/// before the error occurs, if an errorBuilder was intended.
bool _hadErrorHandlerListener = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Private comments start with //, per Flutter's style guide.

Suggested change
/// Tracks if any listener with an onError callback was ever added.
/// Used to prevent reporting errors even if the listener is removed
/// before the error occurs, if an errorBuilder was intended.
bool _hadErrorHandlerListener = false;
// Tracks if any listener with an onError callback was ever added.
// Used to prevent reporting errors even if the listener is removed
// before the error occurs, if an errorBuilder was intended.
bool _hadErrorHandlerListener = false;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the Flutter style guide's Use /// for public-quality private documentation section, /// is appropriate for high-quality private docs aiming for the public standard, so the current format aligns with that guideline.

Comment thread packages/flutter/lib/src/painting/image_stream.dart Outdated
@dkwingsmt

Copy link
Copy Markdown
Contributor

And sorry for the late review! I was in a vacation since mid last week.

@PerLycke

Copy link
Copy Markdown
Author

Hi @dkwingsmt, thank you for the review and feedback!

I understand your concern about the _hadErrorHandlerListener flag perhaps feeling like a workaround. The rationale behind this approach was to work within the current model (where disposal removes listeners, potentially leaving the completer to report errors) by allowing the ImageStreamCompleter to remember the original intent to handle the error (indicated by the presence of an errorBuilder). This flag prevents an unwanted FlutterError report when the listener disappears prematurely due to the disposal race condition highlighted in #97077 (and similar scenarios). Alternatives like addEphemeralErrorListener didn't seem to reliably cover this specific timing issue.

This PR successfully fixes the primary issue where the errorBuilder was effectively ignored, as confirmed by the test case and our production testing.

However, your feedback prompted a deeper look, and I've identified a potential side effect: For the case where errorBuilder is null and the widget is disposed before the error completes, this fix might now suppress the error report from the completer, whereas previously it would likely have been reported. Is this change in behaviour acceptable in the context of fixing #97077 and similar scenarios, given that no explicit handler was defined and the widget context is gone?

While I agree a more explicit stream signalling mechanism might be cleaner architecturally, this PR offers a pragmatic solution for the existing bug within the current API. I'm happy to discuss further or explore other approaches if preferred.

Thanks again for your guidance!

@dkwingsmt

Copy link
Copy Markdown
Contributor

My understanding is that your disagreement mostly comes from

Alternatives like addEphemeralErrorListener didn't seem to reliably cover this specific timing issue.

Can you explain what is not covered? I've drafted a PR #167783 that implements the addEphemeralErrorListener approach, which both passes the unit test you wrote and the reproduction code in #97077 (which reports no errors any more).

@PerLycke

Copy link
Copy Markdown
Author

Hi again @dkwingsmt, thanks again for the feedback and for drafting the alternative fix #167783. I agree that using addEphemeralErrorListener in _resolveImage is a cleaner and more robust solution. It correctly handles the race condition from #97077, and similar scenarios, without modifying ImageStreamCompleter and avoids potential side-effects. I'll close this PR in favour of that approach.

@PerLycke PerLycke closed this Apr 28, 2025
@dkwingsmt

Copy link
Copy Markdown
Contributor

Still thank you very much for you analysis and contribution! They were really helpful :)

github-merge-queue Bot pushed a commit that referenced this pull request May 1, 2025
…h `addEphemeralErrorListener`) (#167783)

_An alternative to #166130 using
`addEphemeralErrorListener`.
The following text is adapted from
#166130

**Problem:**

Currently, when using an `Image` widget with an `errorBuilder`, if the
widget is removed from the widget tree (e.g., due to navigation or
`setState`) *after* the image loading process has started but *before*
an asynchronous loading error is reported back, the error can still be
reported via `FlutterError.reportError`. This occurs because the
`_ImageState` listener is removed upon disposal, and the
`ImageStreamCompleter` subsequently treats the error as unhandled,
logging it despite the developer's intent to handle it via the
`errorBuilder`. This leads to unexpected noise in logs and crash
reporting systems.

**Solution:**

This PR utilizes `addEphemeralErrorListener`, which allows the image
stream to be disposed while having an error reporter. The error will not
be reported as long as there is an error reporter. The `Image` widget
adds an empty error reporter if `errorBuilder` is not null.

**Related Issues:**

* Fixes #97077
* Related: #107416, #69125, #34451,
Baseflow/flutter_cached_network_image#780

**Tests:**

* Added a new test case `errorBuilder prevents FlutterError report even
if widget is disposed` to `test/widgets/image_test.dart` to specifically
verify the fix for the disposal race condition.
* This test was written by @/perlycke in
#166130.
* Existing tests in `test/widgets/image_test.dart` (including golden
tests like 'Failed image loads in debug mode') pass with these changes
without requiring updates.

**Breaking Changes:**

* None. This change fixes incorrect behavior and preserves expected
debug visuals. The internal mechanism for reporting errors when no
`errorBuilder` is present has shifted, but the user-facing outcome is
consistent.


## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework flutter/packages/flutter repository. See also f: labels. will affect goldens Changes to golden files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Navigating to a new page causes the Image errorBuilder to be ignored.

2 participants