Skip to content

fix: handle storage read errors in OfflinePodcastList with error UI and retry (#1954)#2041

Open
vedant7007 wants to merge 3 commits into
SB2318:mainfrom
vedant7007:fix/offline-podcast-empty-catch-1954
Open

fix: handle storage read errors in OfflinePodcastList with error UI and retry (#1954)#2041
vedant7007 wants to merge 3 commits into
SB2318:mainfrom
vedant7007:fix/offline-podcast-empty-catch-1954

Conversation

@vedant7007

Copy link
Copy Markdown
Contributor

Closes #1954

the OfflinePodcastList had an empty catch {} that silently swallowed storage read errors. users with valid downloaded podcasts saw an empty list with a generic 'download some podcasts' prompt when the real problem was a storage read failure — no way to distinguish 'no podcasts' from 'load failed'.

what i fixed:

  • added loadError state to track storage failures
  • catch block now sets a meaningful error message and clears stale podcast data
  • added error UI with the error message and a Retry button that re-attempts the load
  • kept the existing NoOfflinePodcastsState for genuine empty state (no error, no podcasts)
  • added DEV guarded console for debugging

for labels i think gssoc:approved + level:intermediate + type:bug + quality:clean fits here. happy to make changes if needed!

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Thank you @, for creating the PR and contributing to our UltimateHealth project 💗.
Our team will review the PR and will reach out to you soon! 😇
Make sure that you have marked all the tasks that you are done with ✅.
Thank you for your patience! 😀

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Automated Review Feedback

No major issues were identified during this review.

The implementation appears consistent with the repository standards and the modified files were reviewed successfully.

Maintainer Note:

Maintainer @SB2318 will review this pull request after the initial automated review cycle is complete.

@SB2318

SB2318 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

/review

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🤖 Gemini AI Code Review

Summary

This Pull Request addresses a critical issue where storage read errors in OfflinePodcastList were silently swallowed, leading to a confusing user experience where downloaded podcasts might appear missing. The PR introduces a new OfflinePodcastLoadErrorState UI component and integrates it into OfflinePodcastList to display a clear error message and a retry option when storage operations fail. It also correctly distinguishes between a genuine "no podcasts" state and a "load failed" state.

Overall, the PR significantly improves the robustness and user experience of the offline podcast feature by providing proper error feedback. The implementation is clean and follows good practices for error handling in React components.

🔴 High Severity

No high severity issues were identified.

🟡 Medium Severity

  • Issue: Ambiguous readDownloadedPodcasts contract and handling of non-array data.
    The loadPodcasts function includes the line if (!Array.isArray(data)) return;. While this line existed in the original code, its interaction with the new error handling needs clarification. If readDownloadedPodcasts() can return null, undefined, or any non-array value without throwing an error, the current logic will silently return from loadPodcasts. This would result in setPodcasts not being called (leaving podcasts as [] from initial state) and loadError remaining null. Consequently, the NoOfflinePodcastsState would be displayed instead of the OfflinePodcastLoadErrorState, misrepresenting a data format issue as an empty list.

    • Impact: Users might see a "No offline podcasts" message when the actual problem is a corrupted or unexpected data format from storage, preventing them from understanding the true issue or retrying effectively. This blurs the distinction the PR aims to create between "no podcasts" and "load error".
    • Fix: Clarify the expected return contract of readDownloadedPodcasts. If it's strictly expected to return an array (even empty) on success and throw on failure, then any non-array return should be treated as an error.
      // In frontend/src/screens/OfflinePodcastList.tsx, inside loadPodcasts
      const loadPodcasts = async () => {
        try {
          setLoadError(null);
          const data = await readDownloadedPodcasts();
      
          // If readDownloadedPodcasts is strictly typed to return PodcastData[] or throw,
          // then any non-array data indicates an unexpected error.
          if (!Array.isArray(data)) {
            // This case should ideally be caught by the type system or indicate a deeper issue.
            // Treat it as an error to ensure proper feedback.
            throw new Error('Unexpected data format from storage: Expected array.');
          }
          setPodcasts(data);
        } catch (err) {
          if (__DEV__) {
            console.error('Offline podcast load error:', err); // Use console.error for errors
          }
          setPodcasts([]);
          setLoadError(
            err instanceof Error ? err.message : 'Failed to load offline podcasts',
          );
        }
      };
      Alternatively, if readDownloadedPodcasts can legitimately return null or undefined to signify "no data" without an error, then the current if (!Array.isArray(data)) return; is acceptable, but this contract should be explicitly documented. Given the PR's goal, treating non-array as an error seems more aligned.
  • Issue: Potential for sensitive information in error messages.
    The setLoadError logic uses err.message directly. While often safe, if the underlying storage mechanism (e.g., AsyncStorage or a custom wrapper) throws errors with messages containing sensitive file paths, internal system details, or user-specific data, this information could be exposed to the user.

    • Impact: Information leakage, potentially aiding attackers in understanding system internals or exposing user data.
    • Fix: Sanitize or generalize error messages from err.message before displaying them to the user. For production environments, it's often better to show a generic user-friendly message and log the specific err.message internally (e.g., to a crash reporting service).
      // In frontend/src/screens/OfflinePodcastList.tsx, inside loadPodcasts catch block
      setLoadError(
        // Provide a more generic message for the user, log the specific one for debugging
        'Failed to load offline podcasts. Please try again or check your device storage.',
      );
      // For debugging, log the specific error message, but don't expose it directly to the user
      if (__DEV__) {
        console.error('Detailed offline podcast load error:', err);
      } else {
        // In production, send to a logging service if available
        // Sentry.captureException(err);
      }
      If err.message is guaranteed to be user-safe (e.g., only "Storage full", "Permission denied"), then the current approach is acceptable. This depends on the readDownloadedPodcasts implementation.

🟢 Low Severity / Nits

  • Consistency in console logging: In the catch block, consider using console.error instead of console.log for error messages. This provides better semantic meaning and can be filtered more easily in developer tools.
    // In frontend/src/screens/OfflinePodcastList.tsx, inside loadPodcasts catch block
    if (__DEV__) {
      console.error('Offline podcast load error:', err); // Changed to console.error
    }
  • Type for err in catch block: While TypeScript infers err as unknown in catch blocks, explicitly asserting its type or performing a more robust check can improve type safety and readability. The current err instanceof Error is good, but adding a type annotation for err could be considered for consistency if the project has such a linting rule.
    // In frontend/src/screens/OfflinePodcastList.tsx, inside loadPodcasts catch block
    } catch (err: unknown) { // Explicitly type err as unknown
      if (__DEV__) {
        console.error('Offline podcast load error:', err);
      }
      setPodcasts([]);
      setLoadError(
        err instanceof Error ? err.message : 'Failed to load offline podcasts',
      );
    }

What's Good ✅

  1. Clear Separation of Concerns: The new OfflinePodcastLoadErrorState component is well-designed and reusable, encapsulating the error UI logic cleanly. This promotes maintainability and readability.
  2. Effective Error State Management: The PR correctly introduces loadError state and uses it to conditionally render the appropriate empty state (NoOfflinePodcastsState vs. OfflinePodcastLoadErrorState), providing a much-needed distinction for the user.
  3. Developer-Friendly Debugging: The __DEV__ guarded console.log (or console.error) is an excellent practice, ensuring detailed error information is available during development without cluttering production logs or exposing internal details to end-users.

Verdict

Request Changes

The most critical issue is the ambiguous contract of readDownloadedPodcasts and its handling of non-array data. Depending on the actual behavior of readDownloadedPodcasts, the current if (!Array.isArray(data)) return; could lead to an incorrect "no podcasts" state instead of an "error" state, undermining the core goal of this PR. Clarifying this behavior and adjusting the error handling accordingly is important for robustness. Additionally, consider the sanitization of error messages for production.

@SB2318 SB2318 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@vedant7007
The most critical issue is the ambiguous contract of readDownloadedPodcasts and its handling of non-array data. Depending on the actual behavior of readDownloadedPodcasts, the current if (!Array.isArray(data)) return; could lead to an incorrect "no podcasts" state instead of an "error" state, undermining the core goal of this PR. Clarifying this behavior and adjusting the error handling accordingly is important for robustness. Additionally, consider the sanitization of error messages for production.

…cing error message

- removed unreachable !Array.isArray guard since readDownloadedPodcasts is typed to return PodcastDownloadRecord[]
- replaced raw err.message with a generic user message to avoid leaking internal storage errors to the UI
- switched dev log to console.error for better filtering
@vedant7007

Copy link
Copy Markdown
Contributor Author

hey @SB2318 thanks for the review — pushed a follow-up commit addressing the flagged points:

  • ambiguous non-array handling: removed the if (!Array.isArray(data)) return; guard. readDownloadedPodcasts return type is Promise<PodcastDownloadRecord[]> and retrieveItem in MMKVUtils always returns an array (or [] on parse failure), so the check was unreachable. removing it also means any unexpected non-array from a future contract change surfaces via the try/catch instead of being silently ignored
  • error message sanitization: replaced err.message in the UI with a generic Failed to load offline podcasts. Please try again or check your device storage. so we dont leak internal storage error details. the raw error is still logged in dev via console.error for debugging
  • console.log → console.error for the dev log, per the low-severity nit

let me know if anything else needs changing!

@vedant7007

Copy link
Copy Markdown
Contributor Author

hey @SB2318 thanks for the review! ping — the concerns you raised were already addressed in commit 3faa8a1 (pushed after your review):

  1. Ambiguous readDownloadedPodcasts contract — removed the if (!Array.isArray(data)) return; dead-guard entirely. all failures now flow through the catch block into the error UI state, so a non-array return from the helper is treated as an error rather than silently degrading to "no podcasts". OfflinePodcastLoadErrorState is what renders in that case, with a retry button.

  2. Error message sanitization for production — the catch no longer surfaces err.message to users. setLoadError now uses a fixed generic string: "Failed to load offline podcasts. Please try again or check your device storage." — no raw error text reaches the UI. the raw error is only logged behind __DEV__ so it's stripped in production bundles.

diff snapshot on current head:

try {
  setLoadError(null);
  const data = await readDownloadedPodcasts();
  setPodcasts(data);
} catch (err) {
  if (__DEV__) {
    console.error('Offline podcast load error:', err);
  }
  setPodcasts([]);
  setLoadError(
    'Failed to load offline podcasts. Please try again or check your device storage.',
  );
}

CI is red on the standard lockfile drift (yarn install --frozen-lockfile failing because package.json was updated on main without regenerating yarn.lock) — not caused by anything on this branch. Frontend Lint/Typecheck/Tests are skipped downstream of that install failure. Should reproduce on any PR opened against main right now.

lmk if there's anything else you'd like me to change — happy to iterate!

@vedant7007

Copy link
Copy Markdown
Contributor Author

hey @SB2318 — gentle bump on this one, it's been 10 days since the last activity and my follow-up commit 3faa8a1 addressed all three points from your review:

  1. removed the dead !Array.isArray guard so any non-array return now flows into the error UI state (no more silent "no podcasts" for a data-format problem)
  2. sanitized the user-facing error message to a generic string, raw error only logged behind __DEV__
  3. switched dev log from console.logconsole.error

CI on this repo is still red because of a pre-existing yarn install --frozen-lockfile failure on master (unrelated to this PR — same red on my #2090 too). happy to also push a yarn.lock regen commit here on the branch if that would help unblock the merge, or keep it as a separate lockfile PR if you'd prefer.

whenever you have a moment to give it another look — thanks! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] OfflinePodcastList empty catch silently swallows storage read errors — users see empty list instead of error UI

2 participants