diff --git a/msal/application.py b/msal/application.py
index a17d359..66d9b43 100644
--- a/msal/application.py
+++ b/msal/application.py
@@ -633,8 +633,9 @@ class ClientApplication(object):
                 **kwargs)
             if at and "error" not in at:
                 return at
+        last_resp = None
         if app_metadata.get("family_id"):  # Meaning this app belongs to this family
-            at = self._acquire_token_silent_by_finding_specific_refresh_token(
+            last_resp = at = self._acquire_token_silent_by_finding_specific_refresh_token(
                 authority, scopes, dict(query, family_id=app_metadata["family_id"]),
                 **kwargs)
             if at and "error" not in at:
@@ -642,7 +643,8 @@ class ClientApplication(object):
         # Either this app is an orphan, so we will naturally use its own RT;
         # or all attempts above have failed, so we fall back to non-foci behavior.
         return self._acquire_token_silent_by_finding_specific_refresh_token(
-            authority, scopes, dict(query, client_id=self.client_id), **kwargs)
+            authority, scopes, dict(query, client_id=self.client_id),
+            **kwargs) or last_resp
 
     def _get_app_metadata(self, environment):
         apps = self.token_cache.find(  # Use find(), rather than token_cache.get(...)

Refresh token errors are discarded
`acquire_token_silent_with_error` appears to be intended to return the last error response received from AAD, and the documentation implies it does so, but there are cases in which it does not.

For example, if AAD rejects a refresh token, its error response is discarded (`at` is the response):
https://github.com/AzureAD/microsoft-authentication-library-for-python/blob/5c1f0f4ad18a61587393c290b1094b5f8fd014d4/msal/application.py#L601-L609

When AAD rejected the token, that token was evicted from the cache:
https://github.com/AzureAD/microsoft-authentication-library-for-python/blob/5c1f0f4ad18a61587393c290b1094b5f8fd014d4/msal/oauth2cli/oauth2.py#L449-L450

Subsequent calls to `_acquire_token_silent_by_finding_specific_refresh_token` may now return `None` because no refresh token remains in the cache. In such a case, `acquire_token_silent_with_error` returns `None`.

This behavior arguably makes sense because the difference between "no refresh token in the cache" and "only invalid refresh tokens in the cache" may not be interesting. But the documentation on `acquire_token_silent_with_error` implies it will distinguish these cases, so I describe the inconsistency here for comment and clarification.
diff --git a/tests/test_application.py b/tests/test_application.py
index 39becd5..65b36b3 100644
--- a/tests/test_application.py
+++ b/tests/test_application.py
@@ -176,6 +176,19 @@ class TestClientApplicationAcquireTokenSilentFociBehaviors(unittest.TestCase):
 
     # Will not test scenario of app leaving family. Per specs, it won't happen.
 
+    def test_preexisting_family_app_will_attempt_frt_and_return_error(self):
+        error_response = '{"error": "invalid_grant", "error_description": "xyz"}'
+        def tester(url, data=None, **kwargs):
+            self.assertEqual(
+                self.frt, data.get("refresh_token"), "Should attempt the FRT")
+            return MinimalResponse(status_code=400, text=error_response)
+        app = ClientApplication(
+            "preexisting_family_app", authority=self.authority_url, token_cache=self.cache)
+        resp = app._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
+            self.authority, self.scopes, self.account, post=tester)
+        logger.debug("%s.cache = %s", self.id(), self.cache.serialize())
+        self.assertEqual(json.loads(error_response), resp, "Error raised will be returned")
+
     def test_family_app_remove_account(self):
         logger.debug("%s.cache = %s", self.id(), self.cache.serialize())
         app = ClientApplication(

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
cryptography==44.0.2
exceptiongroup==1.2.2
idna==3.10
iniconfig==2.1.0
-e git+https://github.com/AzureAD/microsoft-authentication-library-for-python.git@c235d4e9dea9e4954cf0699bd46bb57ddce959be#egg=msal
packaging==24.2
pluggy==1.5.0
pycparser==2.22
PyJWT==1.7.1
pytest==8.3.5
requests==2.32.3
tomli==2.2.1
urllib3==2.3.0

name: microsoft-authentication-library-for-python
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - cryptography==44.0.2
      - exceptiongroup==1.2.2
      - idna==3.10
      - iniconfig==2.1.0
      - packaging==24.2
      - pluggy==1.5.0
      - pycparser==2.22
      - pyjwt==1.7.1
      - pytest==8.3.5
      - requests==2.32.3
      - tomli==2.2.1
      - urllib3==2.3.0
prefix: /opt/conda/envs/microsoft-authentication-library-for-python

tests/test_application.py::TestClientApplicationAcquireTokenSilentFociBehaviors::test_preexisting_family_app_will_attempt_frt_and_return_error
tests/test_application.py::TokenCacheTestCase::testAddByAad tests/test_application.py::TokenCacheTestCase::testAddByAdfs tests/test_application.py::TokenCacheTestCase::test_key_id_is_also_recorded tests/test_application.py::TestHelperExtractCerts::test_extract_a_tag_enclosed_cert tests/test_application.py::TestHelperExtractCerts::test_extract_a_tag_less_public_cert tests/test_application.py::TestHelperExtractCerts::test_extract_multiple_tag_enclosed_certs tests/test_application.py::TestClientApplicationAcquireTokenSilentErrorBehaviors::test_acquire_token_silent_will_suppress_error tests/test_application.py::TestClientApplicationAcquireTokenSilentErrorBehaviors::test_acquire_token_silent_with_error_will_return_error tests/test_application.py::TestClientApplicationAcquireTokenSilentErrorBehaviors::test_atswe_will_map_some_suberror_to_classification_as_is tests/test_application.py::TestClientApplicationAcquireTokenSilentErrorBehaviors::test_atswe_will_map_some_suberror_to_classification_to_empty_string tests/test_application.py::TestClientApplicationAcquireTokenSilentErrorBehaviors::test_cache_empty_will_be_returned_as_None tests/test_application.py::TestClientApplicationAcquireTokenSilentFociBehaviors::test_family_app_remove_account tests/test_application.py::TestClientApplicationAcquireTokenSilentFociBehaviors::test_known_orphan_app_will_skip_frt_and_only_use_its_own_rt tests/test_application.py::TestClientApplicationAcquireTokenSilentFociBehaviors::test_unknown_family_app_will_attempt_frt_and_join_family tests/test_application.py::TestClientApplicationAcquireTokenSilentFociBehaviors::test_unknown_orphan_app_will_attempt_frt_and_not_remove_it tests/test_application.py::TestClientApplicationForAuthorityMigration::test_acquire_token_silent tests/test_application.py::TestClientApplicationForAuthorityMigration::test_get_accounts
msal/application.py
msal/application.py