Skip to content

Handling Failures

Every failure a codec reports from encode or decode is a CodecException, and more precisely one of four families. The families are not organised by what went wrong or by which method you called — you already know which method you called. They are organised by what you do next, so that each decision a consumer can make is one catch clause:

Person decodeOrNull(byte[] bytes) {
    try {
        return codec.decode(bytes);
    } catch (InvalidPayloadException e) {
        // The bytes are bad — corrupt, truncated, tampered with, or not ours.
        // Quarantine: dead-letter the record, treat the cache entry as a miss.
        return null;
    } catch (UnsupportedFormatException e) {
        // The bytes are fine; this reader is too old for them. Hold the record,
        // route it to a newer reader, or upgrade. Never quarantine: let it
        // propagate so the caller can park the record instead of dropping it.
        throw e;
    } catch (TransientCodecException e) {
        // Something the codec depends on failed — a key service, a JCE provider.
        // The input is not at fault. Retry with backoff, or alert on infrastructure.
        throw e;
    }
}

All four classes live in org.jwcarman.codec, in codec-core — the same artifact that gives you Codec and CodecFactory, so import org.jwcarman.codec.InvalidPayloadException; (and its three siblings) is all a consumer needs; no extra dependency is required just to catch a codec failure.

On the encode side there is one more:

byte[] encodeOrNull(Person value) {
    try {
        return codec.encode(value);
    } catch (InvalidValueException e) {
        // This value cannot be encoded by this codec: a type the backend has no
        // serializer for, a cyclic graph, an unregistered class. Fix the value or
        // the configuration; retrying will not help.
        return null;
    } catch (TransientCodecException e) {
        // As above.
        throw e;
    }
}

catch (CodecException e) is the coarse form — "something went wrong in the codec" — for callers that do not distinguish.

The four families

Family Meaning Your response
InvalidValueException the value handed to encode cannot be encoded fix the value or the configuration
InvalidPayloadException the payload handed to decode is malformed, corrupt, or forged quarantine
UnsupportedFormatException the payload is well-formed but this reader cannot handle it hold, route, upgrade
TransientCodecException something the codec depends on failed; the input is not at fault retry, or alert

All four are unchecked, all extend only CodecException and RuntimeException, and preserve the underlying library's exception as the cause where there is one — the Jackson, Gson, Fory or JCE detail is one getCause() away for most throw sites, but not all: a bad magic number, a bounds check, or a checksum mismatch is detected by this library's own code with no wrapped library exception to report, so getCause() can be null. Either way, you never have to import a library type to handle a codec failure.

Some modules add a more specific subclass where it carries information the family does not: codec-versioned's UnknownVersionException (an UnsupportedFormatException that carries the version), and codec-crypto's DecryptionException (an InvalidPayloadException; see Error taxonomy for why its message is deliberately uniform across cryptographic rejections), KeyAccessException and EncryptionException (both TransientCodecException). You catch the family; the subclass is there when you want to know more.

Why the third family matters

InvalidPayloadException and UnsupportedFormatException look similar — in both cases decode refused the bytes — but they demand opposite responses. During a rolling upgrade, instances that have already been upgraded write payloads the old instances cannot read. If those were reported as invalid, a dead-letter policy would discard every one of them. They are not invalid; they are ahead of you. That is why codec-versioned reports an unregistered version as UnsupportedFormatException, why codec-crypto reports an unknown format version or algorithm id the same way, and why the two families share no parent below CodecException.

What is not a codec failure

Construction and configuration errors are programmer errors at wiring time, not runtime outcomes a pipeline reacts to, and they keep the JDK's types: a bad builder argument is IllegalArgumentException, an inconsistent builder IllegalStateException, and a null handed to a transform's encode or decode, or a null TypeRef handed to a factory, NullPointerException. Whether a backend accepts null is its own business — the JSON backends encode it as the literal null; see Null handling. CodecFactory.create failing for a type it cannot handle is likewise IllegalArgumentException.

Composition passes failures through: andThen, nullSafe and xmap propagate whatever the underlying codec throws. The functions you hand to xmap may throw anything; that is yours.

The adapters propagate too. codec-kafka's deserializer and codec-spring-data-redis's serializer let CodecException through unchanged, families and all — see In practice below for both.

In practice

A Kafka consumer decoding records manually shows where each catch sits around the codec call:

for (ConsumerRecord<String, byte[]> record : records) {
    try {
        Order order = codec.decode(record.value());
        process(order);
    } catch (InvalidPayloadException e) {
        // Poison record: send it to a dead-letter topic and move on.
    } catch (UnsupportedFormatException e) {
        // A newer writer is ahead of this consumer: park the record, alert,
        // do not skip it.
    } catch (TransientCodecException e) {
        // Retry the same record with backoff.
    }
}

Dead-lettering a record, parking a partition, and retrying without losing an offset are Kafka's concerns, not the codec's; see Apache Kafka for how codec-kafka's Deserializer plugs a codec into a consumer.

The cache case is the same shape: codec-spring-data-redis's CodecRedisSerializer lets any CodecException through unchanged, so a CacheErrorHandler decides what happens next. Treating InvalidPayloadException as a miss is reasonable — a corrupted entry is not the caller's fault — but a TransientCodecException should propagate as an error, or a KMS outage looks like an empty cache.

What each module throws

Module Situation Family (and subclass)
codec-transforms gzip/deflate decode a corrupt stream InvalidPayloadException
codec-zstd, codec-lz4 zstd/lz4 decode a corrupt stream InvalidPayloadException
codec-transforms, codec-zstd, codec-lz4 a payload that would expand past the decompression cap InvalidPayloadException
codec-transforms, codec-zstd, codec-lz4 a compressor fails on encode (dependency, not the value) TransientCodecException
codec-transforms ChecksumCodec finds a mismatch or a truncated buffer InvalidPayloadException
codec-transforms Base64/Base32/hex text outside the alphabet; Base32 input that is not canonical (wrong case, bad padding, non-zero trailing bits) InvalidPayloadException
codec-transforms StringCodec decodes bytes not valid in the charset InvalidPayloadException
codec-jackson, codec-jackson2, codec-gson, codec-jsonb malformed JSON on decode InvalidPayloadException
codec-jackson, codec-jackson2, codec-gson, codec-jsonb a value the backend cannot serialize on encode InvalidValueException
codec-protobuf invalid protobuf bytes InvalidPayloadException
codec-fory corrupt or truncated Fory bytes InvalidPayloadException
codec-fory a decoded value of the wrong type InvalidPayloadException
codec-fory an unregistered class on encode InvalidValueException
codec-versioned bad magic or a buffer shorter than the header InvalidPayloadException (VersionedFormatException)
codec-versioned a version this codec has no codec registered for UnsupportedFormatException (UnknownVersionException)
codec-crypto GCM tag mismatch, tampering, or a disallowed keyId InvalidPayloadException (DecryptionException)
codec-crypto an unknown format version or algorithm id UnsupportedFormatException
codec-crypto the KMS is unavailable, or a provider returns an unusable key TransientCodecException (KeyAccessException)
codec-crypto a provider or strategy failure on encode TransientCodecException (EncryptionException)

Adding to the hierarchy

One rule: subclass by what the caller does next. A new failure whose answer is one of the four above is a subclass of that family, never a fifth sibling; a more specific subclass earns its existence only when it carries information the family does not.

Where next