geigerj: Note: this is a breaking change, but since we haven't released `0.14.0` yet, I'm not incrementing the version.
codecov-io: ## [Current coverage](https://codecov.io/gh/googleapis/gax-python/pull/132?src=pr) is 98.05% (diff: 100%)
> Merging [#132](https://codecov.io/gh/googleapis/gax-python/pull/132?src=pr) into [master](https://codecov.io/gh/googleapis/gax-python/branch/master?src=pr) will increase coverage by **0.02%**

```diff
@@             master       #132   diff @@
==========================================
  Files             8          8          
  Lines           608        616     +8   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits            596        604     +8   
  Misses           12         12          
  Partials          0          0          
```

> Powered by [Codecov](https://codecov.io?src=pr). Last update [7b3172b...db0bf04](https://codecov.io/gh/googleapis/gax-python/compare/7b3172b9e8ebd1e513955bf60b42aa5f63d37d4a...db0bf049ff5ba03dc15aa40dc4da7ab02e4efed3?src=pr)
bjwatson: LGTM
diff --git a/google/gax/api_callable.py b/google/gax/api_callable.py
index 622c08e..00c7763 100644
--- a/google/gax/api_callable.py
+++ b/google/gax/api_callable.py
@@ -36,8 +36,8 @@ import time
 from future import utils
 
 from . import (BackoffSettings, BundleOptions, bundling, _CallSettings, config,
-               PageIterator, ResourceIterator, RetryOptions)
-from .errors import GaxError, RetryError
+               errors, PageIterator, ResourceIterator, RetryOptions)
+from .errors import RetryError
 
 _MILLIS_PER_SECOND = 1000
 
@@ -414,12 +414,12 @@ def construct_settings(
     return defaults
 
 
-def _catch_errors(a_func, errors):
+def _catch_errors(a_func, to_catch):
     """Updates a_func to wrap exceptions with GaxError
 
     Args:
         a_func (callable): A callable.
-        retry (list[Exception]): Configures the exceptions to wrap.
+        to_catch (list[Exception]): Configures the exceptions to wrap.
 
     Returns:
         A function that will wrap certain exceptions with GaxError
@@ -429,8 +429,9 @@ def _catch_errors(a_func, errors):
         try:
             return a_func(*args, **kwargs)
         # pylint: disable=catching-non-exception
-        except tuple(errors) as exception:
-            utils.raise_with_traceback(GaxError('RPC failed', cause=exception))
+        except tuple(to_catch) as exception:
+            utils.raise_with_traceback(
+                errors.create_error('RPC failed', cause=exception))
 
     return inner
 
diff --git a/google/gax/config.py b/google/gax/config.py
index 2d94ebe..3a1d34d 100644
--- a/google/gax/config.py
+++ b/google/gax/config.py
@@ -49,6 +49,10 @@ the client constants configuration for retrying into the correct gRPC objects.
 """
 
 
+NAME_STATUS_CODES = grpc.NAME_STATUS_CODES
+"""Inverse map for STATUS_CODE_NAMES"""
+
+
 create_stub = grpc.create_stub  # pylint: disable=invalid-name,
 """The function to use to create stubs."""
 
diff --git a/google/gax/errors.py b/google/gax/errors.py
index c118b1e..79dc12f 100644
--- a/google/gax/errors.py
+++ b/google/gax/errors.py
@@ -29,6 +29,9 @@
 
 """Provides GAX exceptions."""
 
+from __future__ import absolute_import
+from . import config
+
 
 class GaxError(Exception):
     """Common base class for exceptions raised by GAX.
@@ -51,6 +54,25 @@ class GaxError(Exception):
         return 'GaxError({}, caused by {})'.format(msg, self.cause)
 
 
+def create_error(msg, cause=None):
+    """Creates an error.
+
+    Uses a Python built-in exception if one is available, and a
+    GaxError otherwise.
+
+    Attributes:
+      msg (string): describes the error that occurred.
+      cause (Exception, optional): the exception raised by a lower
+        layer of the RPC stack (for example, gRPC) that caused this
+        exception, or None if this exception originated in GAX.
+    """
+    if config.NAME_STATUS_CODES.get(
+            config.exc_to_code(cause)) == 'INVALID_ARGUMENT':
+        return ValueError('{}: {}'.format(msg, cause))
+    else:
+        return GaxError(msg, cause)
+
+
 class RetryError(GaxError):
     """Indicates an error during automatic GAX retrying."""
     pass
diff --git a/google/gax/grpc.py b/google/gax/grpc.py
index 9ec50fb..bb38550 100644
--- a/google/gax/grpc.py
+++ b/google/gax/grpc.py
@@ -58,6 +58,10 @@ STATUS_CODE_NAMES = {
 """Maps strings used in client config to gRPC status codes."""
 
 
+NAME_STATUS_CODES = dict([(v, k) for (k, v) in STATUS_CODE_NAMES.items()])
+"""Inverse map for STATUS_CODE_NAMES"""
+
+
 def exc_to_code(exc):
     """Retrieves the status code from an exception"""
     if not isinstance(exc, RpcError):

Make it clear that the call may fail with the INVALID_ARGUMENT status code
### What:
Wrap the gRPC INVALID_ARGUMENT error with a pythonic VALUE_ERROR. We can do this with a special attribute that we consistently use for the wrapped gRPC error.

### Why:
Be more Pythonic.

See https://github.com/googleapis/googleapis/issues/15
diff --git a/test/test_api_callable.py b/test/test_api_callable.py
index f45c5f4..f30ab66 100644
--- a/test/test_api_callable.py
+++ b/test/test_api_callable.py
@@ -38,6 +38,7 @@ from google.gax import (
     api_callable, bundling, BackoffSettings, BundleDescriptor, BundleOptions,
     _CallSettings, CallOptions, INITIAL_PAGE, PageDescriptor, RetryOptions)
 from google.gax.errors import GaxError, RetryError
+import grpc
 
 
 _SERVICE_NAME = 'test.interface.v1.api'
@@ -484,3 +485,15 @@ class TestCreateApiCallable(unittest2.TestCase):
         other_error_callable = api_callable.create_api_call(
             other_error_func, _CallSettings())
         self.assertRaises(AnotherException, other_error_callable, None)
+
+    def test_wrap_value_error(self):
+
+        invalid_attribute_exc = grpc.RpcError()
+        invalid_attribute_exc.code = lambda: grpc.StatusCode.INVALID_ARGUMENT
+
+        def value_error_func(*dummy_args, **dummy_kwargs):
+            raise invalid_attribute_exc
+
+        value_error_callable = api_callable.create_api_call(
+            value_error_func, _CallSettings())
+        self.assertRaises(ValueError, value_error_callable, None)

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.7
test-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
coverage==7.2.7
exceptiongroup==1.2.2
future==1.0.0
-e git+https://github.com/googleapis/gax-python.git@7b3172b9e8ebd1e513955bf60b42aa5f63d37d4a#egg=google_gax
grpcio==1.62.3
grpcio-tools==1.62.3
httplib2==0.22.0
importlib-metadata==6.7.0
iniconfig==2.0.0
linecache2==1.0.0
mock==5.2.0
oauth2client==4.1.3
packaging==24.0
pluggy==1.2.0
ply==3.8
protobuf==4.24.4
pyasn1==0.5.1
pyasn1-modules==0.3.0
pyparsing==3.1.4
pytest==7.4.4
pytest-cov==4.1.0
pytest-timeout==2.3.1
rsa==4.9
six==1.17.0
tomli==2.0.1
traceback2==1.4.0
typing_extensions==4.7.1
unittest2==1.1.0
zipp==3.15.0

name: gax-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
  - certifi=2022.12.7=py37h06a4308_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=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - argparse==1.4.0
      - coverage==7.2.7
      - exceptiongroup==1.2.2
      - future==1.0.0
      - grpcio==1.62.3
      - grpcio-tools==1.62.3
      - httplib2==0.22.0
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - linecache2==1.0.0
      - mock==5.2.0
      - oauth2client==4.1.3
      - packaging==24.0
      - pluggy==1.2.0
      - ply==3.8
      - protobuf==4.24.4
      - pyasn1==0.5.1
      - pyasn1-modules==0.3.0
      - pyparsing==3.1.4
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - pytest-timeout==2.3.1
      - rsa==4.9
      - six==1.17.0
      - tomli==2.0.1
      - traceback2==1.4.0
      - typing-extensions==4.7.1
      - unittest2==1.1.0
      - zipp==3.15.0
prefix: /opt/conda/envs/gax-python

test/test_api_callable.py::TestCreateApiCallable::test_wrap_value_error
test/test_api_callable.py::TestCreateApiCallable::test_bundling test/test_api_callable.py::TestCreateApiCallable::test_bundling_page_streaming_error test/test_api_callable.py::TestCreateApiCallable::test_call_api_call test/test_api_callable.py::TestCreateApiCallable::test_call_kwargs test/test_api_callable.py::TestCreateApiCallable::test_call_override test/test_api_callable.py::TestCreateApiCallable::test_catch_error test/test_api_callable.py::TestCreateApiCallable::test_construct_settings test/test_api_callable.py::TestCreateApiCallable::test_construct_settings_override test/test_api_callable.py::TestCreateApiCallable::test_construct_settings_override2 test/test_api_callable.py::TestCreateApiCallable::test_no_retry_if_no_codes test/test_api_callable.py::TestCreateApiCallable::test_page_streaming test/test_api_callable.py::TestCreateApiCallable::test_retry test/test_api_callable.py::TestCreateApiCallable::test_retry_aborts_on_unexpected_exception test/test_api_callable.py::TestCreateApiCallable::test_retry_aborts_simple test/test_api_callable.py::TestCreateApiCallable::test_retry_exponential_backoff test/test_api_callable.py::TestCreateApiCallable::test_retry_times_out_no_response test/test_api_callable.py::TestCreateApiCallable::test_retry_times_out_simple
google/gax/api_callable.py google/gax/errors.py google/gax/config.py google/gax/grpc.py
google/gax/api_callable.py google/gax/errors.py google/gax/config.py google/gax/grpc.py