Skip to content

Instantly share code, notes, and snippets.

@ChakshuGautam
Created February 23, 2026 09:06
Show Gist options
  • Select an option

  • Save ChakshuGautam/90684161ddaa086ed5fe2099efaaffb0 to your computer and use it in GitHub Desktop.

Select an option

Save ChakshuGautam/90684161ddaa086ed5fe2099efaaffb0 to your computer and use it in GitHub Desktop.
DIGIT Core: Complete Exception Handling Audit — 40+ instances (swallowed exceptions + wrong error types)

DIGIT Core: Exception Handling Audit

Date: 2026-02-23 Scope: /root/code/Digit-Core and /root/code/digit-2.9lts-core-storm Total Instances Found: 40+

Overview

Category Count Severity
Empty catch blocks 2 CRITICAL
Wrong HTTP status codes 4 CRITICAL
Swallowed with log-only (no re-throw) 15+ HIGH
Swallowed in loops (continue) 8+ HIGH
Specific exception thrown as generic (context lost) 8 HIGH
Exception cause not chained 5 HIGH
Silent null returns 2 MEDIUM
e.printStackTrace() in production 2 MEDIUM
Broad catch (Exception) losing type info 3 MEDIUM

Part A: Swallowed Exceptions

Instances where exceptions (including NullPointerException) are caught and never bubble up.


A1. Empty Catch Block — TracerFilter

File: Digit-Core/core-services/libraries/tracer/src/main/java/org/egov/tracer/http/filters/TracerFilter.java Line: 206

@SuppressWarnings("unchecked")
private String getCorrelationIdFromBody(HttpServletRequest httpServletRequest) {
    String correlationId = null;
    try {
        final HashMap<String, Object> requestMap = (HashMap<String, Object>)
                objectMapper.readValue(httpServletRequest.getInputStream(), HashMap.class);
        Object requestInfo = requestMap.containsKey(REQUEST_INFO_FIELD_NAME_IN_JAVA_CLASS_CASE)
            ? requestMap.get(REQUEST_INFO_FIELD_NAME_IN_JAVA_CLASS_CASE)
            : requestMap.get(REQUEST_INFO_IN_CAMEL_CASE);

        if (isNull(requestInfo))
            return null;
        else {
            if (requestInfo instanceof Map) {
                correlationId = (String) ((Map) requestInfo).get(CORRELATION_ID_FIELD_NAME);
            }
        }
    } catch (IOException ignored){}  // ← SWALLOWED: completely silent
    return correlationId;
}

Impact: IOException (which can wrap NPE during deserialization) is silently eaten. Correlation IDs silently go missing, making distributed tracing unreliable.


A2. Empty Catch Block — DataUploadService (inner catch)

File: Digit-Core/core-services/egov-data-uploader/src/main/java/org/egov/dataupload/service/DataUploadService.java Lines: 567–574

try {
    errors = JsonPath.read(response, "$.Errors");
} catch (PathNotFoundException pe) {
    logger.error("Unable to get Errors object from Error Response, trying Error object");
    try {
        Error error = objectMapper.readValue(response, Error.class);
        failureMessage.append(error.toString());
        failureMessage.append(", ");
    } catch (PathNotFoundException | IOException ignored) {}  // ← SWALLOWED: silent empty block
}

Impact: If the error response can't be parsed at all, the failure reason is lost. Uploads appear to succeed when they actually failed.


A3. Generic Exception Swallowed — DataUploadService (excelDataUpload)

File: Digit-Core/core-services/egov-data-uploader/src/main/java/org/egov/dataupload/service/DataUploadService.java Lines: 238–240

try {
    // ... file processing, row iteration, API calls ...
} catch (IOException e) {
    logger.error("Unable to open file provided.", e);
    uploadJob.setEndTime(new Date().getTime());
    uploadJob.setSuccessfulRows(0);
    uploadJob.setStatus(StatusEnum.fromValue("failed"));
    uploadJob.setReasonForFailure(e.getMessage());
    updateJobsWithPersister(auditDetails, uploadJob, false);
    throw new CustomException(/* ... */);
} catch (Exception e) {
    logger.error("Unknown error ", e);
    // ← SWALLOWED: NPE here means upload silently stops, job status never updated
}

Impact: Any NPE during Excel processing is caught, logged, and then the method returns normally. The upload job status is never set to "failed" — it stays in a zombie state.


A4. Log-Only Catch — WorkflowUtil

File: digit-2.9lts-core-storm/utilities/default-data-handler/src/main/java/org/egov/handler/util/WorkflowUtil.java Lines: 34–37

try {
    restTemplate.postForObject(uri.toString(), businessServiceRequest, Map.class);
} catch (Exception e) {
    log.error("Error creating workflow configuration: {}", e.getMessage());
    // ← SWALLOWED: workflow config creation silently fails
}

Impact: Workflow configuration fails silently. Downstream processes that depend on workflow being configured will fail with confusing errors.


A5. Swallowed in Loop — DataHandlerService (user creation)

File: digit-2.9lts-core-storm/utilities/default-data-handler/src/main/java/org/egov/handler/service/DataHandlerService.java Lines: 132–134

for (JsonNode userNode : userArray) {
    try {
        // ... user creation logic with property accesses on userNode ...
    } catch (Exception e) {
        log.error("Failed to create user from payload: {} | Error: {}", userNode, e.getMessage());
        // ← SWALLOWED: NPE from null userNode fields, loop continues
    }
}

Impact: If a user record has a null field that causes NPE, that user is silently skipped. No summary of which users failed.


A6. Swallowed in Loop — DataHandlerService (schema creation)

File: digit-2.9lts-core-storm/utilities/default-data-handler/src/main/java/org/egov/handler/service/DataHandlerService.java Lines: 254–258

try {
    restTemplate.postForObject(mdmsSchemaCreateUri, request, Object.class);
} catch (Exception innerEx) {
    log.error("Failed to create schema: {} for tenant: {}. Skipping...",
            schemaNode.get("code"), tenantId, innerEx);
    // ← SWALLOWED: schema creation fails silently, "Skipping..."
}

Impact: MDMS schemas silently fail to create. Data lookups depending on these schemas will fail later with no trace back to this root cause.


A7. Swallowed in Loop — DataHandlerService (boundary relationships)

File: digit-2.9lts-core-storm/utilities/default-data-handler/src/main/java/org/egov/handler/service/DataHandlerService.java Lines: 359–362

} catch (Exception ex) {
    log.error("Failed to create individual boundary relationship entry for tenant: {}. Skipping...",
            targetTenantId, ex);
    // ← SWALLOWED: boundary data incomplete, loop continues
}

Impact: Boundary relationships silently go missing. Location-based queries will return incomplete results.


A8. Silent Null Return — ServiceRequestRepository

File: digit-2.9lts-core-storm/utilities/default-data-handler/src/main/java/org/egov/handler/repository/ServiceRequestRepository.java Lines: 38–43

Object response = null;
try {
    response = restTemplate.postForObject(uri.toString(), request, Map.class);
} catch (HttpClientErrorException e) {
    log.error(EXTERNAL_SERVICE_EXCEPTION, e);
    throw new ServiceCallException(e.getResponseBodyAsString());
} catch (Exception e) {
    log.error(SEARCHER_SERVICE_EXCEPTION, e);
    // ← SWALLOWED: generic Exception logged but NOT re-thrown
}
return response;  // ← returns null on failure

Impact: Callers receive null instead of an exception, leading to NPE at the call site. The original cause is lost in logs.


A9. Swallowed in Loop — DataTransformationService (field mapping)

File: Digit-Core/core-services/egov-indexer/src/main/java/org/egov/infra/indexer/service/DataTransformationService.java Lines: 181–184

try {
    documentContext.put(expression, expressionArray[expressionArray.length - 1],
            JsonPath.read(kafkaJson, fieldMapping.getInjsonpath()));
} catch (Exception e) {
    log.error("Error while building custom JSON for index: " + e.getMessage());
    continue;  // ← SWALLOWED: field silently missing from index
}

Impact: Index documents get written with missing fields. Search queries against these fields return no results — silently.


A10. Swallowed in Loop — DataTransformationService (external call)

File: Digit-Core/core-services/egov-indexer/src/main/java/org/egov/infra/indexer/service/DataTransformationService.java Lines: 216–220

try {
    // ... external API call and response parsing ...
    response = mapper.readValue(jsonContent, Map.class);
} catch (Exception e) {
    log.error("Exception while making external call: ", e);
    log.error("URI: " + uri);
    continue;  // ← SWALLOWED: enrichment data missing from index
}

Impact: External enrichment data silently missing from indexed documents.


A11. Swallowed in Loop — DataTransformationService (field mapping response)

File: Digit-Core/core-services/egov-indexer/src/main/java/org/egov/infra/indexer/service/DataTransformationService.java Lines: 234–239

try {
    Object value = JsonPath.read(mapper.writeValueAsString(response), inputJsonPath);
    documentContext.put(expression, expressionArray[expressionArray.length - 1], value);
} catch (Exception e) {
    log.error("Value: " + fieldMapping.getInjsonpath() + " is not found!");
    log.debug("URI: " + uri);
    documentContext.put(expression, expressionArray[expressionArray.length - 1], null);
    continue;  // ← SWALLOWED: null written to index instead of real value
}

Impact: Null values silently replace real data in the index. Particularly dangerous because it actively writes null.


A12. Swallowed in Loop — DataTransformationService (MDMS request)

File: Digit-Core/core-services/egov-indexer/src/main/java/org/egov/infra/indexer/service/DataTransformationService.java Lines: 273–277

try {
    response = indexerUtils.fetchMdmsData(uri, ...);
} catch (Exception e) {
    log.error("Exception while trying to hit: " + uri);
    log.info("MDMS Request failure: " + e);
    continue;  // ← SWALLOWED: MDMS lookup failure hidden
}

Impact: MDMS master data silently missing from indexed records. Reports and dashboards show incomplete data.


A13. Swallowed in Loop — DataTransformationService (MDMS response mapping)

File: Digit-Core/core-services/egov-indexer/src/main/java/org/egov/infra/indexer/service/DataTransformationService.java Lines: 290–295

try {
    Object value = JsonPath.read(mapper.writeValueAsString(response), fieldMapping.getInjsonpath());
    documentContext.put(expression, expressionArray[expressionArray.length - 1], value);
} catch (Exception e) {
    log.error("Value: " + fieldMapping.getInjsonpath() + " is not found!");
    log.debug("MDMS Request: " + request);
    documentContext.put(expression, expressionArray[expressionArray.length - 1], null);
    continue;  // ← SWALLOWED: null written to index, loop continues
}

Impact: Same as A11 — null values silently replace real MDMS-sourced data.


A14. Email Notification Swallowed — UserService

File: Digit-Core/core-services/egov-user/src/main/java/org/egov/user/domain/service/UserService.java Lines: 436–440

if ((oldEmail != null && !oldEmail.isEmpty()) && newEmail != null && !(newEmail.equalsIgnoreCase(oldEmail))) {
    try {
        notificationUtil.sendEmail(requestInfo, existingUser, updatedUser);
    } catch (Exception ignore) {
        log.error("Not able to send email");
        // ← SWALLOWED: user never notified their email changed
    }
}

Impact: Security-sensitive notification (email change) silently fails. User is not informed their contact email was changed.


A15. Test Code — FileStoreRepositoryTest (3 instances)

File: digit-2.9lts-core-storm/digit-oss-sparse/core-services/egov-user/src/test/java/org/egov/user/persistence/repository/FileStoreRepositoryTest.java Lines: 41–44, 58–61, 74–77

try {
    List<String> list = new ArrayList<String>();
    list.add("key");
    fileStoreUrl = fileStoreRepository.getUrlByFileStoreId("default", list);
} catch (Exception e) {
    // TODO Auto-generated catch block
    e.printStackTrace();  // ← SWALLOWED: test silently passes even on failure
}

Impact: Tests that should fail on exception silently pass. False confidence in test coverage.


A16. NPE Risk in Catch Block — UserRequest

File: Digit-Core/core-services/egov-user/src/main/java/org/egov/user/web/contract/UserRequest.java Lines: 212–218

BloodGroup bloodGroup = null;
try {
    if (this.bloodGroup != null)
        bloodGroup = BloodGroup.valueOf(this.bloodGroup.toUpperCase());
} catch (Exception e) {
    bloodGroup = BloodGroup.fromValue(this.bloodGroup);
    // ← NPE RISK: if this.bloodGroup becomes null between check and catch,
    //    or if fromValue() doesn't handle the value gracefully
}

Impact: Enum parsing failure could cascade into NPE in the catch block itself.


A17. Swallowed in Loop — LocalizationUtil

File: digit-2.9lts-core-storm/utilities/default-data-handler/src/main/java/org/egov/handler/util/LocalizationUtil.java Lines: 81–85

try {
    restTemplate.postForObject(uri, createMessagesRequest, ResponseInfo.class);
    log.info("Localization batch [{}-{}] upserted successfully for tenant: {}", i + 1, end, tenantId);
} catch (Exception e) {
    log.error("Failed to upsert localization batch [{}-{}] for tenant: {}. Skipping... Reason: {}",
            i + 1, end, tenantId, e.getMessage());
    // ← SWALLOWED: localization entries silently missing
}

Impact: Localization messages silently fail to load. UI shows raw message codes instead of translated text.


Part B: Wrong Error Types / Misleading Exceptions

Instances where exceptions are caught but re-thrown as the wrong type, with misleading codes, or with lost context.


B1. All CustomExceptions Return 401 — Gateway ExceptionUtils

File: Digit-Core/core-services/gateway/src/main/java/com/example/gateway/utils/ExceptionUtils.java Lines: 67–70

else if (exceptionName.equalsIgnoreCase("CustomException")) {
    CustomException ce = (CustomException) e;
    return _setExceptionBody(exchange, HttpStatus.valueOf(401),
        getErrorInfoObject(exceptionName, exceptionMessage, exceptionMessage));
}

What's wrong: Every CustomException — validation errors, not-found, conflicts, server errors — all return 401 Unauthorized. A tenant not found looks like an auth failure.

Should be:

HttpStatus status = mapErrorCodeToHttpStatus(ce.getCode());
return _setExceptionBody(exchange, status, ...);

B2. JsonProcessingException → RuntimeException (500) — RequestEnrichmentFilterHelper

File: Digit-Core/core-services/gateway/src/main/java/com/example/gateway/filters/pre/helpers/RequestEnrichmentFilterHelper.java Lines: 96–100

try {
    httpHeaders.add(USER_INFO_HEADER_NAME, objectMapper.writeValueAsString(user));
} catch (JsonProcessingException e) {
    throw new RuntimeException(e);
}

What's wrong: JSON serialization failure (likely bad user data — a 400 issue) becomes RuntimeException500 Internal Server Error. Clients can't distinguish "your token has bad data" from "server crashed."

Should be:

throw new CustomException("USER_SERIALIZATION_ERROR",
    "Failed to serialize user info: " + e.getOriginalMessage(), e);

B3. HttpClientErrorException → RuntimeException (loses status code) — FileStoreRepository

File: Digit-Core/core-services/egov-user/src/main/java/org/egov/user/persistence/repository/FileStoreRepository.java Lines: 44–48

try {
    fileStoreUrls = restTemplate.getForObject(Url, Map.class);
} catch (HttpClientErrorException e) {
    throw new RuntimeException(e.getResponseBodyAsString());
}

What's wrong: The HttpClientErrorException carries the original status code (404, 403, etc.) but it's thrown as RuntimeException with just the body string. A 404 from filestore becomes a 500 to the client.

Should be:

catch (HttpClientErrorException e) {
    throw new CustomException("FILESTORE_" + e.getStatusCode().value(),
        "FileStore error: " + e.getResponseBodyAsString(), e);
}

B4. HttpClientErrorException Loses Status Code — OtpRepository

File: Digit-Core/core-services/egov-user/src/main/java/org/egov/user/persistence/repository/OtpRepository.java Lines: 60–62

catch (HttpClientErrorException e) {
    log.error("Otp validation failed", e);
    throw new ServiceCallException(e.getResponseBodyAsString());
}

What's wrong: HttpClientErrorException includes the HTTP status (401 for invalid OTP, 429 for rate limit, 503 for service down). Only the body is passed to ServiceCallException — caller can't distinguish "wrong OTP" from "OTP service down."

Should be:

throw new ServiceCallException(e.getStatusCode() + ": " + e.getResponseBodyAsString(), e);

B5. DataAccessException → Generic "Unable to fetch" — TransactionService

File: Digit-Core/core-services/egov-pg-service/src/main/java/org/egov/pg/service/TransactionService.java Lines: 115–120

try {
    return transactionRepository.fetchTransactions(transactionCriteria);
} catch (DataAccessException e) {
    log.error("Unable to fetch data from the database for criteria: " + transactionCriteria, e);
    throw new CustomException("FETCH_TXNS_FAILED", "Unable to fetch transactions from store");
}

What's wrong: DataAccessException has specific subtypes — QueryTimeoutException, DataIntegrityViolationException, CannotAcquireLockException, etc. All are flattened to "Unable to fetch transactions from store." A timeout looks the same as a constraint violation.

Should be:

throw new CustomException("DB_FETCH_FAILED",
    "Transaction query failed: " + e.getMostSpecificCause().getMessage(), e);

B6. Different Exceptions → Same Error Code — ElasticSearchRepository

File: Digit-Core/core-services/national-dashboard-ingest/src/main/java/org/egov/nationaldashboardingest/repository/ElasticSearchRepository.java Lines: 67–73

catch (ResourceAccessException e) {
    log.error("ES is down");
    throw new CustomException("EG_ES_ERR", "Elastic search is down");
} catch (Exception e) {
    log.error("Exception while indexing data onto ES.");
    throw new CustomException("EG_ES_IDX_ERR", e.getMessage());
}

What's wrong: "ES is down" (connectivity) vs "indexing error" (data format) need different error codes and different handling by the caller. The first might warrant a retry; the second needs data fixes. Neither chains the cause.

Should be:

catch (ResourceAccessException e) {
    throw new CustomException("ES_CONNECTION_FAILED", "ElasticSearch unreachable", e);
} catch (JsonProcessingException e) {
    throw new CustomException("ES_DATA_FORMAT_ERR", "Invalid data for indexing: " + e.getMessage(), e);
} catch (Exception e) {
    throw new CustomException("ES_INDEXING_ERR", "Unexpected indexing error", e);
}

B7. IOException → CustomException (cause not chained) — CustomRequestWrapper

File: Digit-Core/core-services/zuul/src/main/java/org/egov/wrapper/CustomRequestWrapper.java Lines: 22–26

try {
    payload = IOUtils.toString(request.getInputStream());
} catch (IOException e) {
    throw new CustomException("INPUT_TO_STRING_CONVERSION_ERROR", e.getMessage());
}

What's wrong: The IOException cause is not chained. The stack trace of the original error is lost. When this shows up in logs, you can't tell where the IOException originated.

Should be:

throw new CustomException("INPUT_TO_STRING_CONVERSION_ERROR",
    "Failed to read request payload", e);  // chain cause

B8. JsonProcessingException → ServiceCallException (wrong type) — IndexerUtils

File: Digit-Core/core-services/egov-indexer/src/main/java/org/egov/infra/indexer/util/IndexerUtils.java Lines: 325–330

catch (JsonProcessingException e) {
    log.error("JsonProcessingException: ", e);
    throw new ServiceCallException(e.getMessage());
}

What's wrong: JsonProcessingException (a parsing/serialization error) is thrown as ServiceCallException (implies a remote service call failed). The caller will retry the service call when the actual issue is malformed JSON.

Should be:

throw new CustomException("JSON_PARSE_ERROR",
    "Failed to parse response: " + e.getOriginalMessage(), e);

B9. JsonProcessingException → Generic "INVALID_GEOJSON" — BoundaryEntityValidator

File: Digit-Core/core-services/boundary-service/src/main/java/digit/service/validator/BoundaryEntityValidator.java Lines: 73–87

try {
    if (boundary.getGeometry().get(BoundaryConstants.TYPE).asText().equals(BoundaryConstants.POINT)) {
        GeoUtil.validatePointGeometry(objectMapper.treeToValue(boundary.getGeometry(), PointGeometry.class));
    }
    // ...
} catch (JsonProcessingException e) {
    throw new CustomException(ErrorCodes.INVALID_GEOJSON_CODE, ErrorCodes.INVALID_GEOJSON_MSG);
}

What's wrong: The JsonProcessingException tells you exactly which field was wrong and why. The generic "INVALID_GEOJSON" message gives the API consumer no actionable information.

Should be:

throw new CustomException(ErrorCodes.INVALID_GEOJSON_CODE,
    ErrorCodes.INVALID_GEOJSON_MSG + ": " + e.getOriginalMessage(), e);

B10. ClassCastException Swallowed with Misleading Log — JacksonUtils

File: Digit-Core/core-services/libraries/enc-client/src/main/java/org/egov/encryption/util/JacksonUtils.java Lines: 113–115

catch (ClassCastException e) {
    log.error("Cannot find value for path : " + filterPath);
}

What's wrong: ClassCastException is caught, a misleading message ("Cannot find value" — it's actually a type mismatch, not a missing value) is logged, and the method silently returns null. Encryption/decryption silently produces incomplete data.

Should be:

catch (ClassCastException e) {
    throw new CustomException("JSON_TYPE_MISMATCH",
        "Type mismatch at path: " + filterPath + " - expected compatible type", e);
}

B11. printStackTrace() + Kafka Error Topic — PersisterAuditClientService

File: Digit-Core/core-services/audit-service/src/main/java/org/egov/auditservice/persisterauditclient/PersisterAuditClientService.java Lines: 84–97

catch (Exception e) {
    e.printStackTrace();  // ← anti-pattern in production
    log.error("AUDIT_LOG_ERROR", "Failed to create audit log for: " + rowDataList);
    AuditError auditError = AuditError.builder()
        // ...builds error object...
        .build();
    kafkaTemplate.send(auditErrorTopic, auditError);
}

What's wrong: (1) e.printStackTrace() writes to stderr outside the logging framework — these messages are often lost in containerized environments. (2) The AuditError sent to Kafka doesn't include the exception stack trace, so the error topic consumer can't diagnose the issue.

Should be:

catch (Exception e) {
    log.error("Failed to create audit log for: {}", rowDataList, e);
    AuditError auditError = AuditError.builder()
        .exceptionMessage(e.getMessage())
        .exceptionStackTrace(ExceptionUtils.getStackTrace(e))
        .build();
    kafkaTemplate.send(auditErrorTopic, auditError);
}

B12. printStackTrace() + Silent Failure — URLConverterService

File: Digit-Core/core-services/egov-url-shortening/src/main/java/org/egov/url/shortening/service/URLConverterService.java Lines: 98–103

try {
    urlRepository.saveUrl("url:" + id, shortenRequest);
} catch (JsonProcessingException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

What's wrong: URL save fails silently. The API returns a shortened URL that doesn't actually exist in the database. When someone clicks the short URL, they get a 404 — but the original API call returned success.

Should be:

catch (JsonProcessingException e) {
    throw new CustomException("URL_SAVE_FAILED",
        "Failed to persist shortened URL: " + e.getMessage(), e);
}

B13. Inconsistent Re-throw — ReportRepository

File: Digit-Core/core-services/report/src/main/java/org/egov/report/repository/ReportRepository.java Lines: 104–130

catch (DataAccessResourceFailureException ex) {
    PSQLException cause = (PSQLException) ex.getCause();
    if (cause != null && cause.getSQLState().equals("57014")) {
        throw new CustomException("QUERY_EXECUTION_TIMEOUT", "Query failed...");
    } else {
        throw ex;  // ← raw DataAccessResourceFailureException escapes
    }
} catch (Exception e) {
    throw new CustomException("QUERY_EXEC_ERROR", "Error while executing query: " + e.getMessage());
}

What's wrong: The non-timeout branch throws raw DataAccessResourceFailureException while all other branches throw CustomException. This means callers get inconsistent error formats depending on the specific database failure.

Should be:

} else {
    throw new CustomException("DB_ACCESS_FAILURE",
        "Database query failed: " + ex.getMostSpecificCause().getMessage(), ex);
}

B14. Broad Exception → Generic Validation Error — MdmsDataValidator

File: Digit-Core/core-services/mdms-v2/src/main/java/org/egov/infra/mdms/service/validator/MdmsDataValidator.java Lines: 89–91

catch (Exception e) {
    throw new CustomException("MASTER_DATA_VALIDATION_ERR",
        "An unknown error occurred while validating provided master data against the schema - " + e.getMessage());
}

What's wrong: The word "unknown" in a user-facing error message is a red flag. ValidationException (bad data), JsonProcessingException (bad JSON format), and NullPointerException (missing schema) all get the same "unknown error" message.

Should be:

catch (ValidationException e) {
    throw new CustomException("SCHEMA_VALIDATION_FAILED", e.getErrorMessage());
} catch (JsonProcessingException e) {
    throw new CustomException("INVALID_JSON_FORMAT", "Data is not valid JSON: " + e.getMessage(), e);
} catch (Exception e) {
    throw new CustomException("MASTER_DATA_VALIDATION_ERR",
        "Validation failed unexpectedly: " + e.getMessage(), e);
}

Part C: Systemic Issues

C1. CustomException Never Chains the Cause

Across the codebase, CustomException is almost always thrown as:

throw new CustomException("CODE", "message");

Instead of:

throw new CustomException("CODE", "message", originalException);

This means stack traces are lost everywhere. When you see CustomException in logs, you can never trace back to the original failure.

C2. No Error Code → HTTP Status Mapping

The gateway maps ALL CustomException to 401. There's no mechanism to return 400 (bad request), 404 (not found), or 409 (conflict) from service-layer exceptions.

C3. ServiceCallException vs CustomException Ambiguity

Some services throw ServiceCallException for parsing errors, others throw CustomException for service call failures. There's no consistent taxonomy.


Part D: Recommendations

Priority Matrix

Priority Fix Impact
P0 Fix gateway ExceptionUtils — stop returning 401 for everything (B1) All API consumers get correct status codes
P0 Chain exception causes in all CustomException throws (C1) Stack traces available for debugging
P0 Fix empty catch blocks (A1, A2) Failures become visible
P0 Fix zombie job state in DataUploadService (A3) Upload jobs report correct status
P1 Fix silent null returns (A8) Stop NPE cascades at call sites
P1 Fix index data corruption — DataTransformationService (A9–A13) Indexed documents have complete data
P1 Fix security notification swallowing (A14) Users notified of email changes
P1 Replace RuntimeException throws with CustomException (B2, B3) Consistent error format
P1 Replace printStackTrace() with proper logging (B11, B12) Errors visible in log aggregation
P2 Fix loop swallowing — collect errors and report summary (A5–A7, A17) Batch failures reported, not hidden
P2 Fix workflow config swallowing (A4) Tenant setup fails visibly
P2 Split broad catch (Exception) into specific catches (B6, B14) Better error messages for API consumers
P2 Establish error code taxonomy (C3) Consistent error handling across services
P3 Fix test code — let exceptions propagate (A15) Accurate test coverage

Patterns to Apply

For swallowed exceptions — re-throw:

// BEFORE:
} catch (Exception e) {
    log.error("Something failed", e);
}

// AFTER:
} catch (Exception e) {
    log.error("Something failed", e);
    throw new CustomException("DESCRIPTIVE_CODE", "Something failed: " + e.getMessage(), e);
}

For loops with partial failure — collect and report:

List<String> errors = new ArrayList<>();
for (...) {
    try { ... }
    catch (Exception e) {
        errors.add("Item X failed: " + e.getMessage());
        log.error("...", e);
    }
}
if (!errors.isEmpty()) {
    throw new CustomException("BATCH_PARTIAL_FAILURE",
        errors.size() + " of " + total + " items failed: " + String.join("; ", errors));
}

For exception cause chaining — always pass the cause:

// BEFORE:
throw new CustomException("CODE", e.getMessage());

// AFTER:
throw new CustomException("CODE", "Meaningful message", e);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment