Repository: pydata/xarray

Issue:
# `open_dataset` should fail early, before reaching `guess_engine` when file doesn't exist

### What happened?

I ran a code in a script from long ago that uses `xr.open_dataset`, and I gave the script a path to a file that doesn't exist, and got this error:

```
ValueError: did not find a match in any of xarray's currently installed IO backends ['h5netcdf', 'scipy', 'zarr']. Consider explicitly selecting one of the installed engines via the ``engine`` parameter, or installing additional IO dependencies, see:
https://docs.xarray.dev/en/stable/getting-started-guide/installing.html
https://docs.xarray.dev/en/stable/user-guide/io.html
```

This made me think that maybe `xarray` got updated and the engine guessing algorithm got different. But apparently I was wrong, and the `open_dataset` file simply don't exist.

### What did you expect to happen?

`open_dataset` to fail earlier saying there is no file in the given path, and print the path too in the error message.

### Minimal Complete Verifiable Example

```Python
import xarray as xr
xr.show_versions()

xr.open_dataset("missing.h5")
```

### Steps to reproduce

I cannot use `uv` to show demonstrate the issue (I use NixOS), but the issue should be simple enough for you to forgive me hopefully. It might be that installing an HDF5 backend is required, but I'm not sure.

### MVCE confirmation

- [x] Minimal example — the example is as focused as reasonably possible to demonstrate the underlying issue in xarray.
- [x] Complete example — the example is self-contained, including all data and the text of any traceback.
- [x] Verifiable example — the example copy & pastes into an IPython prompt or [Binder notebook](https://mybinder.org/v2/gh/pydata/xarray/main?urlpath=lab/tree/doc/examples/blank_template.ipynb), returning the result.
- [x] New issue — a search of GitHub Issues suggests this is not a duplicate.
- [ ] Recent environment — the issue occurs with the latest version of xarray and its dependencies.

### Relevant log output

```Python
ValueError: did not find a match in any of xarray's currently installed IO backends ['h5netcdf', 'scipy', 'zarr']. Consider explicitly selecting one of the installed engines via the ``engine`` parameter, or installing additional IO dependencies, see:
https://docs.xarray.dev/en/stable/getting-started-guide/installing.html
https://docs.xarray.dev/en/stable/user-guide/io.html
```

### Anything else we need to know?

_No response_

### Environment

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.13.8 (main, Oct  7 2025, 12:01:51) [GCC 14.3.0]
python-bits: 64
OS: Linux
OS-release: 6.12.56
machine: x86_64
processor:
byteorder: little
LC_ALL: None
LANG: en_IL
LOCALE: ('en_IL', 'ISO8859-1')
libhdf5: 1.14.6
libnetcdf: None

xarray: 2025.7.1
pandas: 2.3.1
numpy: 2.3.3
scipy: 1.16.2
netCDF4: None
pydap: None
h5netcdf: 1.6.4
h5py: 3.14.0
zarr: 3.1.1
cftime: None
nc_time_axis: None
iris: None
bottleneck: None
dask: None
distributed: None
matplotlib: 3.10.5
cartopy: None
seaborn: None
numbagg: None
fsspec: None
cupy: None
pint: 0.25.0.dev0
sparse: None
flox: None
numpy_groupies: None
setuptools: None
pip: None
conda: None
pytest: None
mypy: 1.17.1
IPython: None
sphinx: None

</details>

Implementation patch:
diff --git a/xarray/backends/plugins.py b/xarray/backends/plugins.py
index 6e1784af5b6..db79dbae7ff 100644
--- a/xarray/backends/plugins.py
+++ b/xarray/backends/plugins.py
@@ -3,6 +3,7 @@
 import functools
 import inspect
 import itertools
+import os
 import warnings
 from collections.abc import Callable
 from importlib.metadata import entry_points
@@ -10,10 +11,9 @@
 
 from xarray.backends.common import BACKEND_ENTRYPOINTS, BackendEntrypoint
 from xarray.core.options import OPTIONS
-from xarray.core.utils import module_available
+from xarray.core.utils import is_remote_uri, module_available
 
 if TYPE_CHECKING:
-    import os
     from importlib.metadata import EntryPoint, EntryPoints
 
     from xarray.backends.common import AbstractDataStore
@@ -209,6 +209,11 @@ def guess_engine(
             "https://docs.xarray.dev/en/stable/getting-started-guide/installing.html"
         )
 
+    if isinstance(store_spec, str | os.PathLike):
+        store_spec_str = str(store_spec)
+        if not is_remote_uri(store_spec_str) and not os.path.exists(store_spec_str):
+            raise FileNotFoundError(f"No such file: '{store_spec_str}'")
+
     raise ValueError(error_msg)
 
 


Test patch:
diff --git a/xarray/tests/test_plugins.py b/xarray/tests/test_plugins.py
index e20f665c9ff..5bb17c06eb2 100644
--- a/xarray/tests/test_plugins.py
+++ b/xarray/tests/test_plugins.py
@@ -188,24 +188,66 @@ def test_build_engines_sorted() -> None:
     "xarray.backends.plugins.list_engines",
     mock.MagicMock(return_value={"dummy": DummyBackendEntrypointArgs()}),
 )
-def test_no_matching_engine_found() -> None:
-    with pytest.raises(ValueError, match=r"did not find a match in any"):
+def test_no_matching_engine_found(tmp_path) -> None:
+    # Non-existent local file raises FileNotFoundError
+    with pytest.raises(FileNotFoundError, match=r"No such file"):
         plugins.guess_engine("not-valid")
 
+    # Existing file with unrecognized extension raises ValueError
+    existing_file = tmp_path / "test.unknown"
+    existing_file.write_bytes(b"")
+    with pytest.raises(ValueError, match=r"did not find a match in any"):
+        plugins.guess_engine(str(existing_file))
+
+    # Existing file with recognized magic number raises ValueError
+    nc_file = tmp_path / "foo.nc"
+    nc_file.write_bytes(b"CDF\x01\x00\x00\x00\x00")
     with pytest.raises(ValueError, match=r"found the following matches with the input"):
-        plugins.guess_engine("foo.nc")
+        plugins.guess_engine(str(nc_file))
 
 
 @mock.patch(
     "xarray.backends.plugins.list_engines",
     mock.MagicMock(return_value={}),
 )
-def test_engines_not_installed() -> None:
-    with pytest.raises(ValueError, match=r"xarray is unable to open"):
+def test_engines_not_installed(tmp_path) -> None:
+    # Non-existent local file raises FileNotFoundError
+    with pytest.raises(FileNotFoundError, match=r"No such file"):
         plugins.guess_engine("not-valid")
 
+    # Existing file with no matching engine raises ValueError
+    existing_file = tmp_path / "test.unknown"
+    existing_file.write_bytes(b"")
+    with pytest.raises(ValueError, match=r"xarray is unable to open"):
+        plugins.guess_engine(str(existing_file))
+
+    # Existing file with recognized magic number raises ValueError
+    nc_file = tmp_path / "foo.nc"
+    nc_file.write_bytes(b"CDF\x01\x00\x00\x00\x00")
     with pytest.raises(ValueError, match=r"found the following matches with the input"):
-        plugins.guess_engine("foo.nc")
+        plugins.guess_engine(str(nc_file))
+
+
+@mock.patch(
+    "xarray.backends.plugins.list_engines",
+    mock.MagicMock(return_value={"dummy": DummyBackendEntrypointArgs()}),
+)
+def test_guess_engine_file_not_found() -> None:
+    # Non-existent local file path (string)
+    with pytest.raises(
+        FileNotFoundError, match=r"No such file: '/nonexistent/path.h5'"
+    ):
+        plugins.guess_engine("/nonexistent/path.h5")
+
+    # Non-existent local file path (PathLike)
+    from pathlib import Path
+
+    with pytest.raises(FileNotFoundError, match=r"No such file"):
+        plugins.guess_engine(Path("/nonexistent/path.h5"))
+
+    # Remote URIs should not raise FileNotFoundError (raises ValueError instead)
+    with pytest.raises(ValueError):
+        plugins.guess_engine("https://example.com/missing.h5")
 
 
 @pytest.mark.parametrize("engine", common.BACKEND_ENTRYPOINTS.keys())