diff --git a/newsfragments/1573.bugfix.rst b/newsfragments/1573.bugfix.rst
new file mode 100644
index 00000000..26fdec2b
--- /dev/null
+++ b/newsfragments/1573.bugfix.rst
@@ -0,0 +1,1 @@
+Added a helpful error message if an async function is passed to `trio.to_thread.run_sync`.
\ No newline at end of file
diff --git a/trio/_threads.py b/trio/_threads.py
index 8867388b..0ce2a7e3 100644
--- a/trio/_threads.py
+++ b/trio/_threads.py
@@ -179,7 +179,17 @@ async def to_thread_run_sync(sync_fn, *args, cancellable=False, limiter=None):
     def worker_fn():
         TOKEN_LOCAL.token = current_trio_token
         try:
-            return sync_fn(*args)
+            ret = sync_fn(*args)
+
+            if inspect.iscoroutine(ret):
+                # Manually close coroutine to avoid RuntimeWarnings
+                ret.close()
+                raise TypeError(
+                    "Trio expected a sync function, but {!r} appears to be "
+                    "asynchronous".format(getattr(sync_fn, "__qualname__", sync_fn))
+                )
+
+            return ret
         finally:
             del TOKEN_LOCAL.token
 

Give a better diagnostic if user does trio.to_thread.run_sync(async_fn)
#1513 fixed the `trio.from_thread` functions to give nice diagnostics here, but skipped `trio.to_thread`.

Here's a real user who got confused by this: https://gitter.im/python-trio/general?at=5ed860e2b101510b203789ef

diff --git a/trio/tests/test_threads.py b/trio/tests/test_threads.py
index a9059fef..55f01eea 100644
--- a/trio/tests/test_threads.py
+++ b/trio/tests/test_threads.py
@@ -457,6 +457,15 @@ async def test_trio_to_thread_run_sync_token():
     assert callee_token == caller_token
 
 
+async def test_trio_to_thread_run_sync_expected_error():
+    # Test correct error when passed async function
+    async def async_fn():  # pragma: no cover
+        pass
+
+    with pytest.raises(TypeError, match="expected a sync function"):
+        await to_thread_run_sync(async_fn)
+
+
 async def test_trio_from_thread_run_sync():
     # Test that to_thread_run_sync correctly "hands off" the trio token to
     # trio.from_thread.run_sync()

pip install -e .[dev]
parse_log_pytest
pytest
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
async-generator==1.10
attrs==25.3.0
coverage==7.8.0
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
execnet==2.1.1
idna==3.10
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
outcome==1.3.0.post0
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pytest @ file:///croot/pytest_1738938843180/work
pytest-asyncio==0.26.0
pytest-cov==6.0.0
pytest-mock==3.14.0
pytest-xdist==3.6.1
sniffio==1.3.1
sortedcontainers==2.4.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
-e git+https://github.com/python-trio/trio.git@08fca2a69592d2c8a0aee24ef26f1a09be495866#egg=trio
typing_extensions==4.13.0

name: trio
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
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_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
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=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
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - async-generator==1.10
      - attrs==25.3.0
      - coverage==7.8.0
      - execnet==2.1.1
      - idna==3.10
      - outcome==1.3.0.post0
      - pytest-asyncio==0.26.0
      - pytest-cov==6.0.0
      - pytest-mock==3.14.0
      - pytest-xdist==3.6.1
      - sniffio==1.3.1
      - sortedcontainers==2.4.0
      - trio==0.15.1+dev
      - typing-extensions==4.13.0
prefix: /opt/conda/envs/trio

trio/tests/test_threads.py::test_trio_to_thread_run_sync_expected_error
trio/tests/test_threads.py::test_do_in_trio_thread trio/tests/test_threads.py::test_do_in_trio_thread_from_trio_thread trio/tests/test_threads.py::test_run_in_trio_thread_ki trio/tests/test_threads.py::test_await_in_trio_thread_while_main_exits trio/tests/test_threads.py::test_run_in_worker_thread trio/tests/test_threads.py::test_run_in_worker_thread_cancellation trio/tests/test_threads.py::test_run_in_worker_thread_abandoned trio/tests/test_threads.py::test_run_in_worker_thread_limiter[False-False-3] trio/tests/test_threads.py::test_run_in_worker_thread_limiter[False-False-5] trio/tests/test_threads.py::test_run_in_worker_thread_limiter[False-False-10] trio/tests/test_threads.py::test_run_in_worker_thread_limiter[False-True-3] trio/tests/test_threads.py::test_run_in_worker_thread_limiter[False-True-5] trio/tests/test_threads.py::test_run_in_worker_thread_limiter[False-True-10] trio/tests/test_threads.py::test_run_in_worker_thread_limiter[True-False-3] trio/tests/test_threads.py::test_run_in_worker_thread_limiter[True-False-5] trio/tests/test_threads.py::test_run_in_worker_thread_limiter[True-False-10] trio/tests/test_threads.py::test_run_in_worker_thread_limiter[True-True-3] trio/tests/test_threads.py::test_run_in_worker_thread_limiter[True-True-5] trio/tests/test_threads.py::test_run_in_worker_thread_limiter[True-True-10] trio/tests/test_threads.py::test_run_in_worker_thread_custom_limiter trio/tests/test_threads.py::test_run_in_worker_thread_limiter_error trio/tests/test_threads.py::test_run_in_worker_thread_fail_to_spawn trio/tests/test_threads.py::test_trio_to_thread_run_sync_token trio/tests/test_threads.py::test_trio_from_thread_run_sync trio/tests/test_threads.py::test_trio_from_thread_run trio/tests/test_threads.py::test_trio_from_thread_token trio/tests/test_threads.py::test_trio_from_thread_token_kwarg trio/tests/test_threads.py::test_from_thread_no_token trio/tests/test_threads.py::test_run_fn_as_system_task_catched_badly_typed_token trio/tests/test_threads.py::test_do_in_trio_thread_from_trio_thread_legacy trio/tests/test_threads.py::test_BlockingTrioPortal_with_explicit_TrioToken trio/tests/test_threads.py::test_BlockingTrioPortal_deprecated_export
newsfragments/1573.bugfix.rst trio/_threads.py
newsfragments/1573.bugfix.rst trio/_threads.py