openlang

except ValueError never runs around gather(return_exceptions=True), then TypeError on the ValueError

I run a small script before client check-ins that scores each client's site from three metrics, with per-client weights fetched from their analytics endpoint. The fetches go through asyncio.gather with return_exceptions=True, so one broken client doesn't cancel the rest, right? fetch_weights raises ValueError when the response is empty, and I wrapped the gather in try/except ValueError to log a warning and fall back to the default weights.

The first client prints a score, then the script dies on eastside-cycles, which has an empty response. There's no "falling back to defaults" warning at all. I changed it to except Exception, and I ran it with python3 -X dev main.py, and neither changed anything.

The part I don't get is that the TypeError mentions a ValueError object, even though the ValueError is clearly raised in fetch_weights.

juniper-florals: 83.0
Traceback (most recent call last):
  File "main.py", line 59, in <module>
    main()
  File "main.py", line 55, in main
    asyncio.run(score_all(clients))
  ...
  File "main.py", line 40, in score_all
    value = score(client["metrics"], weights)
  File "main.py", line 26, in score
    return round(sum(metrics[k] * weights[k] for k in weights), 1)
TypeError: 'ValueError' object is not iterable

The relevant part of main.py:

async def score_all(clients):
    try:
        all_weights = await asyncio.gather(
            *(fetch_weights(c) for c in clients),
            return_exceptions=True,
        )
    except ValueError:
        log.warning("weights fetch failed, falling back to defaults")
        all_weights = [DEFAULT_WEIGHTS for _ in clients]

    for client, weights in zip(clients, all_weights):
        value = score(client["metrics"], weights)
        print(client["slug"] + ": " + str(value))

Why is the handler skipped?

The conversation continues 5

Follow the reasoning
  1. Permalink

    With return_exceptions=True, gather never raises the ValueError. It hands it back as an item in the result list, so the except never fires. Then score() gets the exception object where it expects weights, hence the TypeError. Probably check isinstance(w, Exception) per item and fall back there.

    all_weights = [
        DEFAULT_WEIGHTS if isinstance(w, Exception) else w
        for w in all_weights
    ]
  2. Permalink

    One caveat on my own snippet: isinstance(w, Exception) is probably broader than you want. Any failure, like a timeout or a bad auth token, would quietly get default weights and look like a normal score. Since the warning is the whole point, I'd check for ValueError specifically and log the client slug. Anything else can then blow up loudly.

    for c, w in zip(clients, all_weights):
        if isinstance(w, ValueError):
            log.warning("empty weights for %s, using defaults", c["slug"])

    In maintenance work I've seen a catch-all fallback like this hide a broken endpoint for weeks.

  3. Permalink

    Thanks, checking per item makes sense, right? For other exceptions, should I re-raise right away, or log them all after the loop?

  4. Permalink

    Probably re-raise right away for anything that isn't a ValueError. If you log everything after the loop, you've already scored clients with bad data by then. Same goes for the ones you skip, so I'd fail fast there.

    for c, w in zip(clients, all_weights):
        if isinstance(w, ValueError):
            log.warning("empty weights for %s, using defaults", c["slug"])
            w = DEFAULT_WEIGHTS
        elif isinstance(w, BaseException):
            raise w

Other sides of the same question

  1. `String::from_utf8` gives the bytes back when it fails

    A consuming API has to decide what happens to the value when it fails. String::from_utf8 takes the Vec<u8> by value, and on f…

    Replies 0Latest activity rust
  2. `result.map_error` on every line of a use chain

    use with result.try is my favourite bit of Gleam and I'm starting to resent it a little. Every step calls a different module,…

    Replies 1Latest activity gleam
  3. An errdefer free leaked on the success path

    A test leaked memory, and the leak was on the path that succeeded. The function allocated a scratch buffer, parsed into a res…

    Replies 2Latest activity zig