json_decode returns null on failure, and null is also a perfectly valid decoded value — so the only correct check was a separate function call that nobody remembered to make.
// before
$data = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException(json_last_error_msg());
}
// 7.3
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
$json = json_encode($payload, JSON_THROW_ON_ERROR);
The exception is a JsonException, which is a Throwable rather than an Exception subclass of anything domain-specific, so it will pass through a catch (Exception) unchanged. Encoding throws too, which catches the case people forget entirely — invalid UTF-8 in a string silently produced false and an empty response body. Adding the flag to a wrapper function once is better than adding it at four hundred call sites, and the wrapper is the place to translate it into something your own code understands.