1. xml
  2. /best practices
  3. /error-handling

XML Error Handling

Proper error handling is crucial for building robust XML applications that can gracefully handle various error conditions including malformed XML, validation failures, parsing errors, and I/O problems. Good error handling improves user experience, aids debugging, and ensures application stability.

This guide covers best practices for handling XML errors across different parsing methods and scenarios.

Types of XML Errors

Parsing Errors

Common XML parsing errors include:

  • Well-formedness errors: Invalid XML structure
  • Validation errors: Document doesn't conform to schema
  • Encoding errors: Character encoding issues
  • I/O errors: File access or network problems
  • Memory errors: Document too large for available memory

Error Classification

public enum XMLErrorType {
    WELL_FORMEDNESS("XML structure is invalid"),
    VALIDATION("Document doesn't conform to schema"),
    ENCODING("Character encoding error"),
    IO("Input/output error"),
    MEMORY("Insufficient memory"),
    SECURITY("Security constraint violation"),
    TIMEOUT("Operation timed out"),
    UNKNOWN("Unknown error occurred");
    
    private final String description;
    
    XMLErrorType(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
}

public class XMLError {
    private final XMLErrorType type;
    private final String message;
    private final int lineNumber;
    private final int columnNumber;
    private final Throwable cause;
    private final String context;
    
    public XMLError(XMLErrorType type, String message, int lineNumber, int columnNumber, 
                   Throwable cause, String context) {
        this.type = type;
        this.message = message;
        this.lineNumber = lineNumber;
        this.columnNumber = columnNumber;
        this.cause = cause;
        this.context = context;
    }
    
    // getters and utility methods
}

DOM Error Handling

Comprehensive DOM Error Handling

public class RobustDOMProcessor {
    private static final Logger logger = LoggerFactory.getLogger(RobustDOMProcessor.class);
    
    public ProcessingResult processXMLDocument(String filePath) {
        try {
            Document document = parseDocumentSafely(filePath);
            
            if (document == null) {
                return ProcessingResult.failure("Failed to parse document");
            }
            
            List<DataRecord> records = extractDataSafely(document);
            return ProcessingResult.success(records);
            
        } catch (XMLProcessingException e) {
            logger.error("XML processing failed for file: {}", filePath, e);
            return ProcessingResult.failure(e.getMessage(), e.getErrorType());
            
        } catch (Exception e) {
            logger.error("Unexpected error processing XML file: {}", filePath, e);
            return ProcessingResult.failure("Unexpected error: " + e.getMessage());
        }
    }
    
    private Document parseDocumentSafely(String filePath) throws XMLProcessingException {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setValidating(false);
            factory.setNamespaceAware(true);
            
            DocumentBuilder builder = factory.newDocumentBuilder();
            
            // Set custom error handler
            XMLErrorHandler errorHandler = new XMLErrorHandler();
            builder.setErrorHandler(errorHandler);
            
            // Parse with timeout
            Document document = parseWithTimeout(builder, filePath, 30000); // 30 second timeout
            
            // Check if errors occurred during parsing
            if (errorHandler.hasErrors()) {
                throw new XMLProcessingException(
                    XMLErrorType.WELL_FORMEDNESS,
                    "Parsing errors occurred: " + errorHandler.getErrorSummary(),
                    errorHandler.getErrors()
                );
            }
            
            return document;
            
        } catch (ParserConfigurationException e) {
            throw new XMLProcessingException(XMLErrorType.UNKNOWN, 
                "Parser configuration error", e);
                
        } catch (SAXException e) {
            throw new XMLProcessingException(XMLErrorType.WELL_FORMEDNESS, 
                "XML structure error: " + e.getMessage(), e);
                
        } catch (IOException e) {
            throw new XMLProcessingException(XMLErrorType.IO, 
                "File access error: " + e.getMessage(), e);
        }
    }
    
    private Document parseWithTimeout(DocumentBuilder builder, String filePath, long timeoutMs) 
            throws IOException, SAXException {
        
        ExecutorService executor = Executors.newSingleThreadExecutor();
        
        try {
            Future<Document> future = executor.submit(() -> {
                try {
                    return builder.parse(filePath);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });
            
            return future.get(timeoutMs, TimeUnit.MILLISECONDS);
            
        } catch (TimeoutException e) {
            throw new IOException("Parsing timed out after " + timeoutMs + "ms");
        } catch (InterruptedException | ExecutionException e) {
            throw new IOException("Parsing interrupted: " + e.getMessage(), e);
        } finally {
            executor.shutdown();
        }
    }
    
    private List<DataRecord> extractDataSafely(Document document) throws XMLProcessingException {
        List<DataRecord> records = new ArrayList<>();
        List<XMLError> extractionErrors = new ArrayList<>();
        
        try {
            XPath xpath = XPathFactory.newInstance().newXPath();
            NodeList recordNodes = (NodeList) xpath.evaluate("//record", document, XPathConstants.NODESET);
            
            for (int i = 0; i < recordNodes.getLength(); i++) {
                try {
                    Element recordElement = (Element) recordNodes.item(i);
                    DataRecord record = extractSingleRecord(recordElement, i + 1);
                    records.add(record);
                    
                } catch (Exception e) {
                    XMLError error = new XMLError(
                        XMLErrorType.VALIDATION,
                        "Failed to extract record " + (i + 1) + ": " + e.getMessage(),
                        -1, -1, e, "record extraction"
                    );
                    extractionErrors.add(error);
                    
                    // Continue processing other records
                    logger.warn("Failed to extract record {}: {}", i + 1, e.getMessage());
                }
            }
            
            // If too many extraction errors, fail the operation
            if (extractionErrors.size() > recordNodes.getLength() * 0.5) {
                throw new XMLProcessingException(
                    XMLErrorType.VALIDATION,
                    "Too many extraction errors (" + extractionErrors.size() + " out of " + recordNodes.getLength() + ")",
                    extractionErrors
                );
            }
            
        } catch (XPathExpressionException e) {
            throw new XMLProcessingException(XMLErrorType.UNKNOWN, 
                "XPath expression error", e);
        }
        
        return records;
    }
    
    private DataRecord extractSingleRecord(Element element, int recordNumber) throws ValidationException {
        DataRecord record = new DataRecord();
        
        // Validate required fields
        String id = getRequiredAttribute(element, "id", recordNumber);
        record.setId(id);
        
        String name = getRequiredElementText(element, "name", recordNumber);
        record.setName(name);
        
        // Optional fields with default values
        String description = getOptionalElementText(element, "description", "");
        record.setDescription(description);
        
        return record;
    }
    
    private String getRequiredAttribute(Element element, String attributeName, int recordNumber) 
            throws ValidationException {
        String value = element.getAttribute(attributeName);
        if (value == null || value.trim().isEmpty()) {
            throw new ValidationException(
                "Required attribute '" + attributeName + "' missing in record " + recordNumber
            );
        }
        return value.trim();
    }
    
    private String getRequiredElementText(Element parent, String elementName, int recordNumber) 
            throws ValidationException {
        NodeList elements = parent.getElementsByTagName(elementName);
        if (elements.getLength() == 0) {
            throw new ValidationException(
                "Required element '" + elementName + "' missing in record " + recordNumber
            );
        }
        
        String text = elements.item(0).getTextContent();
        if (text == null || text.trim().isEmpty()) {
            throw new ValidationException(
                "Required element '" + elementName + "' is empty in record " + recordNumber
            );
        }
        
        return text.trim();
    }
    
    private String getOptionalElementText(Element parent, String elementName, String defaultValue) {
        NodeList elements = parent.getElementsByTagName(elementName);
        if (elements.getLength() == 0) {
            return defaultValue;
        }
        
        String text = elements.item(0).getTextContent();
        return (text != null && !text.trim().isEmpty()) ? text.trim() : defaultValue;
    }
}

Custom Error Handler

public class XMLErrorHandler implements ErrorHandler {
    private final List<XMLError> errors = new ArrayList<>();
    private final List<XMLError> warnings = new ArrayList<>();
    private final boolean failOnError;
    private final boolean failOnWarning;
    
    public XMLErrorHandler() {
        this(true, false);
    }
    
    public XMLErrorHandler(boolean failOnError, boolean failOnWarning) {
        this.failOnError = failOnError;
        this.failOnWarning = failOnWarning;
    }
    
    @Override
    public void warning(SAXParseException exception) throws SAXException {
        XMLError warning = createXMLError(XMLErrorType.VALIDATION, exception, "warning");
        warnings.add(warning);
        
        logger.warn("XML parsing warning at line {}: {}", 
                   exception.getLineNumber(), exception.getMessage());
        
        if (failOnWarning) {
            throw new SAXException("Warning treated as error: " + exception.getMessage(), exception);
        }
    }
    
    @Override
    public void error(SAXParseException exception) throws SAXException {
        XMLError error = createXMLError(XMLErrorType.VALIDATION, exception, "error");
        errors.add(error);
        
        logger.error("XML parsing error at line {}: {}", 
                    exception.getLineNumber(), exception.getMessage());
        
        if (failOnError) {
            throw new SAXException("Parsing error: " + exception.getMessage(), exception);
        }
    }
    
    @Override
    public void fatalError(SAXParseException exception) throws SAXException {
        XMLError fatalError = createXMLError(XMLErrorType.WELL_FORMEDNESS, exception, "fatal error");
        errors.add(fatalError);
        
        logger.error("XML fatal error at line {}: {}", 
                    exception.getLineNumber(), exception.getMessage());
        
        // Always fail on fatal errors
        throw new SAXException("Fatal parsing error: " + exception.getMessage(), exception);
    }
    
    private XMLError createXMLError(XMLErrorType type, SAXParseException exception, String context) {
        return new XMLError(
            type,
            exception.getMessage(),
            exception.getLineNumber(),
            exception.getColumnNumber(),
            exception,
            context
        );
    }
    
    public boolean hasErrors() {
        return !errors.isEmpty();
    }
    
    public boolean hasWarnings() {
        return !warnings.isEmpty();
    }
    
    public List<XMLError> getErrors() {
        return new ArrayList<>(errors);
    }
    
    public List<XMLError> getWarnings() {
        return new ArrayList<>(warnings);
    }
    
    public String getErrorSummary() {
        StringBuilder summary = new StringBuilder();
        
        if (!errors.isEmpty()) {
            summary.append("Errors: ").append(errors.size());
        }
        
        if (!warnings.isEmpty()) {
            if (summary.length() > 0) {
                summary.append(", ");
            }
            summary.append("Warnings: ").append(warnings.size());
        }
        
        return summary.toString();
    }
}

SAX Error Handling

Robust SAX Processing

public class RobustSAXProcessor {
    private static final Logger logger = LoggerFactory.getLogger(RobustSAXProcessor.class);
    
    public ProcessingResult processXMLStream(String filePath) {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        factory.setNamespaceAware(true);
        
        try {
            SAXParser parser = factory.newSAXParser();
            
            RobustSAXHandler handler = new RobustSAXHandler();
            XMLErrorHandler errorHandler = new XMLErrorHandler(false, false); // Don't fail immediately
            
            parser.getXMLReader().setErrorHandler(errorHandler);
            parser.parse(filePath, handler);
            
            // Check for processing errors
            List<XMLError> allErrors = new ArrayList<>();
            allErrors.addAll(errorHandler.getErrors());
            allErrors.addAll(handler.getProcessingErrors());
            
            if (!allErrors.isEmpty()) {
                logger.warn("Processing completed with {} errors", allErrors.size());
                
                // Decide whether to treat as success or failure based on error severity
                if (hasOnlyWarnings(allErrors)) {
                    return ProcessingResult.successWithWarnings(handler.getRecords(), allErrors);
                } else {
                    return ProcessingResult.failure("Processing errors occurred", allErrors);
                }
            }
            
            return ProcessingResult.success(handler.getRecords());
            
        } catch (ParserConfigurationException e) {
            logger.error("Parser configuration error", e);
            return ProcessingResult.failure("Parser configuration error: " + e.getMessage());
            
        } catch (SAXException e) {
            logger.error("SAX parsing error", e);
            return ProcessingResult.failure("Parsing error: " + e.getMessage());
            
        } catch (IOException e) {
            logger.error("I/O error during parsing", e);
            return ProcessingResult.failure("File access error: " + e.getMessage());
            
        } catch (Exception e) {
            logger.error("Unexpected error during SAX processing", e);
            return ProcessingResult.failure("Unexpected error: " + e.getMessage());
        }
    }
    
    private boolean hasOnlyWarnings(List<XMLError> errors) {
        return errors.stream().allMatch(error -> 
            error.getType() == XMLErrorType.VALIDATION && 
            error.getContext().equals("warning"));
    }
}

public class RobustSAXHandler extends DefaultHandler {
    private final List<DataRecord> records = new ArrayList<>();
    private final List<XMLError> processingErrors = new ArrayList<>();
    private final StringBuilder currentText = new StringBuilder();
    
    private DataRecord currentRecord;
    private String currentElement;
    private int currentRecordNumber = 0;
    
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) 
            throws SAXException {
        currentElement = qName;
        currentText.setLength(0);
        
        if ("record".equals(qName)) {
            currentRecordNumber++;
            currentRecord = new DataRecord();
            
            // Process attributes safely
            try {
                processRecordAttributes(attributes);
            } catch (Exception e) {
                XMLError error = new XMLError(
                    XMLErrorType.VALIDATION,
                    "Error processing attributes for record " + currentRecordNumber + ": " + e.getMessage(),
                    -1, -1, e, "attribute processing"
                );
                processingErrors.add(error);
                
                // Continue with default values
                currentRecord.setId("unknown-" + currentRecordNumber);
            }
        }
    }
    
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        currentText.append(ch, start, length);
    }
    
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        try {
            String text = currentText.toString().trim();
            
            if ("record".equals(qName)) {
                // Validate and add record
                validateAndAddRecord();
                
            } else if (currentRecord != null) {
                // Process field within record
                processRecordField(qName, text);
            }
            
        } catch (Exception e) {
            XMLError error = new XMLError(
                XMLErrorType.VALIDATION,
                "Error processing element '" + qName + "' in record " + currentRecordNumber + ": " + e.getMessage(),
                -1, -1, e, "element processing"
            );
            processingErrors.add(error);
        }
        
        currentElement = null;
    }
    
    private void processRecordAttributes(Attributes attributes) {
        String id = attributes.getValue("id");
        if (id == null || id.trim().isEmpty()) {
            throw new IllegalArgumentException("Record ID is required");
        }
        currentRecord.setId(id);
        
        String type = attributes.getValue("type");
        if (type != null) {
            currentRecord.setType(type);
        }
    }
    
    private void processRecordField(String fieldName, String fieldValue) {
        switch (fieldName) {
            case "name":
                if (fieldValue.isEmpty()) {
                    throw new IllegalArgumentException("Name field cannot be empty");
                }
                currentRecord.setName(fieldValue);
                break;
                
            case "description":
                currentRecord.setDescription(fieldValue);
                break;
                
            case "value":
                try {
                    double value = Double.parseDouble(fieldValue);
                    currentRecord.setValue(value);
                } catch (NumberFormatException e) {
                    throw new IllegalArgumentException("Invalid numeric value: " + fieldValue);
                }
                break;
                
            default:
                // Handle unknown fields gracefully
                currentRecord.addCustomField(fieldName, fieldValue);
        }
    }
    
    private void validateAndAddRecord() {
        try {
            // Validate required fields
            if (currentRecord.getId() == null) {
                throw new IllegalStateException("Record ID is missing");
            }
            
            if (currentRecord.getName() == null) {
                throw new IllegalStateException("Record name is missing");
            }
            
            records.add(currentRecord);
            
        } catch (Exception e) {
            XMLError error = new XMLError(
                XMLErrorType.VALIDATION,
                "Record validation failed for record " + currentRecordNumber + ": " + e.getMessage(),
                -1, -1, e, "record validation"
            );
            processingErrors.add(error);
            
            // Optionally add record with default values
            addRecordWithDefaults();
        } finally {
            currentRecord = null;
        }
    }
    
    private void addRecordWithDefaults() {
        if (currentRecord.getId() == null) {
            currentRecord.setId("error-record-" + currentRecordNumber);
        }
        if (currentRecord.getName() == null) {
            currentRecord.setName("Unknown");
        }
        
        records.add(currentRecord);
    }
    
    public List<DataRecord> getRecords() {
        return new ArrayList<>(records);
    }
    
    public List<XMLError> getProcessingErrors() {
        return new ArrayList<>(processingErrors);
    }
}

StAX Error Handling

Resilient StAX Processing

public class ResilientStAXProcessor {
    private static final Logger logger = LoggerFactory.getLogger(ResilientStAXProcessor.class);
    
    public ProcessingResult processLargeXMLFile(String filePath) {
        XMLInputFactory factory = XMLInputFactory.newInstance();
        
        // Configure factory for better error handling
        factory.setProperty(XMLInputFactory.IS_COALESCING, false);
        factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true);
        
        List<DataRecord> records = new ArrayList<>();
        List<XMLError> processingErrors = new ArrayList<>();
        
        try (FileInputStream fis = new FileInputStream(filePath);
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            
            XMLStreamReader reader = factory.createXMLStreamReader(bis);
            
            int recordNumber = 0;
            
            while (reader.hasNext()) {
                try {
                    int event = reader.next();
                    
                    if (event == XMLStreamConstants.START_ELEMENT && 
                        "record".equals(reader.getLocalName())) {
                        
                        recordNumber++;
                        
                        try {
                            DataRecord record = processRecord(reader, recordNumber);
                            records.add(record);
                            
                        } catch (XMLStreamException e) {
                            XMLError error = createProcessingError(e, recordNumber, "record processing");
                            processingErrors.add(error);
                            
                            // Try to recover by skipping to next record
                            skipToNextRecord(reader);
                            
                        } catch (ValidationException e) {
                            XMLError error = new XMLError(
                                XMLErrorType.VALIDATION,
                                "Validation error in record " + recordNumber + ": " + e.getMessage(),
                                -1, -1, e, "record validation"
                            );
                            processingErrors.add(error);
                            
                            // Continue processing
                        }
                    }
                    
                } catch (XMLStreamException e) {
                    XMLError error = createProcessingError(e, recordNumber, "stream processing");
                    processingErrors.add(error);
                    
                    // Try to recover
                    if (!attemptStreamRecovery(reader, e)) {
                        break; // Cannot recover, stop processing
                    }
                }
            }
            
        } catch (FileNotFoundException e) {
            logger.error("File not found: {}", filePath, e);
            return ProcessingResult.failure("File not found: " + filePath);
            
        } catch (IOException e) {
            logger.error("I/O error processing file: {}", filePath, e);
            return ProcessingResult.failure("I/O error: " + e.getMessage());
            
        } catch (XMLStreamException e) {
            logger.error("XML stream error", e);
            return ProcessingResult.failure("XML stream error: " + e.getMessage());
            
        } catch (Exception e) {
            logger.error("Unexpected error processing file: {}", filePath, e);
            return ProcessingResult.failure("Unexpected error: " + e.getMessage());
        }
        
        // Determine result based on success rate
        if (records.isEmpty()) {
            return ProcessingResult.failure("No records processed successfully");
        }
        
        if (processingErrors.isEmpty()) {
            return ProcessingResult.success(records);
        }
        
        // Success with warnings if error rate is acceptable
        double errorRate = (double) processingErrors.size() / (records.size() + processingErrors.size());
        if (errorRate < 0.1) { // Less than 10% error rate
            return ProcessingResult.successWithWarnings(records, processingErrors);
        } else {
            return ProcessingResult.failure("Too many processing errors (" + processingErrors.size() + ")", processingErrors);
        }
    }
    
    private DataRecord processRecord(XMLStreamReader reader, int recordNumber) 
            throws XMLStreamException, ValidationException {
        
        DataRecord record = new DataRecord();
        
        // Process attributes
        int attributeCount = reader.getAttributeCount();
        for (int i = 0; i < attributeCount; i++) {
            String attrName = reader.getAttributeLocalName(i);
            String attrValue = reader.getAttributeValue(i);
            
            if ("id".equals(attrName)) {
                if (attrValue == null || attrValue.trim().isEmpty()) {
                    throw new ValidationException("Record ID is required");
                }
                record.setId(attrValue);
            }
        }
        
        // Process child elements
        while (reader.hasNext()) {
            int event = reader.next();
            
            if (event == XMLStreamConstants.END_ELEMENT && 
                "record".equals(reader.getLocalName())) {
                break;
            }
            
            if (event == XMLStreamConstants.START_ELEMENT) {
                String elementName = reader.getLocalName();
                String elementValue = getElementTextSafely(reader);
                
                processRecordElement(record, elementName, elementValue, recordNumber);
            }
        }
        
        // Validate record
        validateRecord(record, recordNumber);
        
        return record;
    }
    
    private String getElementTextSafely(XMLStreamReader reader) throws XMLStreamException {
        try {
            return reader.getElementText();
        } catch (XMLStreamException e) {
            // Handle cases where element text cannot be read
            logger.warn("Failed to read element text: {}", e.getMessage());
            return "";
        }
    }
    
    private void processRecordElement(DataRecord record, String elementName, String elementValue, int recordNumber) 
            throws ValidationException {
        
        try {
            switch (elementName) {
                case "name":
                    if (elementValue.trim().isEmpty()) {
                        throw new ValidationException("Name cannot be empty");
                    }
                    record.setName(elementValue);
                    break;
                    
                case "description":
                    record.setDescription(elementValue);
                    break;
                    
                case "value":
                    try {
                        double value = Double.parseDouble(elementValue);
                        record.setValue(value);
                    } catch (NumberFormatException e) {
                        throw new ValidationException("Invalid numeric value: " + elementValue);
                    }
                    break;
                    
                default:
                    // Handle unknown elements gracefully
                    record.addCustomField(elementName, elementValue);
            }
            
        } catch (Exception e) {
            throw new ValidationException("Error processing element '" + elementName + "': " + e.getMessage(), e);
        }
    }
    
    private void validateRecord(DataRecord record, int recordNumber) throws ValidationException {
        if (record.getId() == null) {
            throw new ValidationException("Record " + recordNumber + " is missing required ID");
        }
        
        if (record.getName() == null) {
            throw new ValidationException("Record " + recordNumber + " is missing required name");
        }
    }
    
    private XMLError createProcessingError(XMLStreamException e, int recordNumber, String context) {
        Location location = e.getLocation();
        int lineNumber = location != null ? location.getLineNumber() : -1;
        int columnNumber = location != null ? location.getColumnNumber() : -1;
        
        return new XMLError(
            XMLErrorType.WELL_FORMEDNESS,
            "Processing error in record " + recordNumber + ": " + e.getMessage(),
            lineNumber,
            columnNumber,
            e,
            context
        );
    }
    
    private void skipToNextRecord(XMLStreamReader reader) throws XMLStreamException {
        int depth = 1;
        
        while (reader.hasNext() && depth > 0) {
            int event = reader.next();
            
            if (event == XMLStreamConstants.START_ELEMENT) {
                depth++;
            } else if (event == XMLStreamConstants.END_ELEMENT) {
                depth--;
            }
        }
    }
    
    private boolean attemptStreamRecovery(XMLStreamReader reader, XMLStreamException originalError) {
        try {
            // Try to skip to next element
            while (reader.hasNext()) {
                int event = reader.next();
                if (event == XMLStreamConstants.START_ELEMENT) {
                    return true; // Successfully recovered
                }
            }
        } catch (XMLStreamException e) {
            logger.error("Failed to recover from stream error", e);
        }
        
        return false; // Cannot recover
    }
}

Error Recovery Strategies

Graceful Degradation

public class GracefulXMLProcessor {
    
    public ProcessingResult processWithGracefulDegradation(String filePath) {
        List<ProcessingStrategy> strategies = Arrays.asList(
            new OptimizedStAXStrategy(),
            new RobustSAXStrategy(),
            new FallbackDOMStrategy()
        );
        
        List<XMLError> allErrors = new ArrayList<>();
        
        for (ProcessingStrategy strategy : strategies) {
            try {
                ProcessingResult result = strategy.process(filePath);
                
                if (result.isSuccess()) {
                    if (!allErrors.isEmpty()) {
                        // Add previous errors as warnings
                        result.addWarnings(allErrors);
                    }
                    return result;
                }
                
                allErrors.addAll(result.getErrors());
                
            } catch (Exception e) {
                XMLError error = new XMLError(
                    XMLErrorType.UNKNOWN,
                    "Strategy " + strategy.getClass().getSimpleName() + " failed: " + e.getMessage(),
                    -1, -1, e, "strategy execution"
                );
                allErrors.add(error);
            }
        }
        
        return ProcessingResult.failure("All processing strategies failed", allErrors);
    }
}

public abstract class ProcessingStrategy {
    public abstract ProcessingResult process(String filePath) throws Exception;
}

public class OptimizedStAXStrategy extends ProcessingStrategy {
    @Override
    public ProcessingResult process(String filePath) throws Exception {
        // Fast StAX processing with minimal error handling
        return new ResilientStAXProcessor().processLargeXMLFile(filePath);
    }
}

public class RobustSAXStrategy extends ProcessingStrategy {
    @Override
    public ProcessingResult process(String filePath) throws Exception {
        // SAX processing with comprehensive error handling
        return new RobustSAXProcessor().processXMLStream(filePath);
    }
}

public class FallbackDOMStrategy extends ProcessingStrategy {
    @Override
    public ProcessingResult process(String filePath) throws Exception {
        // DOM processing as last resort for smaller files
        File file = new File(filePath);
        if (file.length() > 50 * 1024 * 1024) { // 50MB limit
            throw new IllegalArgumentException("File too large for DOM processing");
        }
        
        return new RobustDOMProcessor().processXMLDocument(filePath);
    }
}

Error Reporting and Logging

Comprehensive Error Reporting

public class XMLErrorReporter {
    private static final Logger logger = LoggerFactory.getLogger(XMLErrorReporter.class);
    
    public void generateErrorReport(List<XMLError> errors, String context) {
        if (errors.isEmpty()) {
            return;
        }
        
        logger.info("Generating error report for context: {}", context);
        
        // Group errors by type
        Map<XMLErrorType, List<XMLError>> errorsByType = errors.stream()
            .collect(Collectors.groupingBy(XMLError::getType));
        
        // Generate summary
        ErrorReportSummary summary = createErrorSummary(errorsByType);
        logger.info("Error summary: {}", summary);
        
        // Log detailed errors
        for (Map.Entry<XMLErrorType, List<XMLError>> entry : errorsByType.entrySet()) {
            XMLErrorType type = entry.getKey();
            List<XMLError> typeErrors = entry.getValue();
            
            logger.error("=== {} Errors ({}) ===", type, typeErrors.size());
            
            for (XMLError error : typeErrors) {
                logDetailedError(error);
            }
        }
        
        // Generate recommendations
        List<String> recommendations = generateRecommendations(errorsByType);
        if (!recommendations.isEmpty()) {
            logger.info("Recommendations:");
            recommendations.forEach(rec -> logger.info("- {}", rec));
        }
    }
    
    private ErrorReportSummary createErrorSummary(Map<XMLErrorType, List<XMLError>> errorsByType) {
        ErrorReportSummary summary = new ErrorReportSummary();
        
        for (Map.Entry<XMLErrorType, List<XMLError>> entry : errorsByType.entrySet()) {
            summary.addErrorCount(entry.getKey(), entry.getValue().size());
        }
        
        return summary;
    }
    
    private void logDetailedError(XMLError error) {
        StringBuilder logMessage = new StringBuilder();
        logMessage.append("Error: ").append(error.getMessage());
        
        if (error.getLineNumber() > 0) {
            logMessage.append(" [Line: ").append(error.getLineNumber());
            
            if (error.getColumnNumber() > 0) {
                logMessage.append(", Column: ").append(error.getColumnNumber());
            }
            
            logMessage.append("]");
        }
        
        if (error.getContext() != null) {
            logMessage.append(" [Context: ").append(error.getContext()).append("]");
        }
        
        logger.error(logMessage.toString());
        
        if (error.getCause() != null && logger.isDebugEnabled()) {
            logger.debug("Error cause:", error.getCause());
        }
    }
    
    private List<String> generateRecommendations(Map<XMLErrorType, List<XMLError>> errorsByType) {
        List<String> recommendations = new ArrayList<>();
        
        if (errorsByType.containsKey(XMLErrorType.WELL_FORMEDNESS)) {
            recommendations.add("Check XML document structure and ensure all tags are properly closed");
            recommendations.add("Validate XML using an XML validator before processing");
        }
        
        if (errorsByType.containsKey(XMLErrorType.VALIDATION)) {
            recommendations.add("Review schema requirements and ensure document conforms");
            recommendations.add("Check for missing required elements or attributes");
        }
        
        if (errorsByType.containsKey(XMLErrorType.ENCODING)) {
            recommendations.add("Verify character encoding is correctly specified");
            recommendations.add("Check for invalid characters in the document");
        }
        
        if (errorsByType.containsKey(XMLErrorType.MEMORY)) {
            recommendations.add("Consider using streaming parsers (SAX/StAX) for large documents");
            recommendations.add("Increase JVM heap size or process document in chunks");
        }
        
        if (errorsByType.containsKey(XMLErrorType.IO)) {
            recommendations.add("Check file permissions and network connectivity");
            recommendations.add("Verify file path and availability");
        }
        
        return recommendations;
    }
}

Error Metrics and Monitoring

public class XMLErrorMetrics {
    private final AtomicLong totalErrors = new AtomicLong(0);
    private final Map<XMLErrorType, AtomicLong> errorCountsByType = new ConcurrentHashMap<>();
    private final CircularBuffer<XMLError> recentErrors = new CircularBuffer<>(100);
    
    public void recordError(XMLError error) {
        totalErrors.incrementAndGet();
        
        errorCountsByType.computeIfAbsent(error.getType(), k -> new AtomicLong(0))
                        .incrementAndGet();
        
        recentErrors.add(error);
        
        // Alert if error rate is too high
        checkErrorRate();
    }
    
    private void checkErrorRate() {
        long recentErrorCount = recentErrors.size();
        
        if (recentErrorCount >= 50) { // More than 50 errors in recent buffer
            logger.warn("High error rate detected: {} recent errors", recentErrorCount);
            
            // Group recent errors for analysis
            Map<XMLErrorType, Long> recentErrorTypes = recentErrors.stream()
                .collect(Collectors.groupingBy(XMLError::getType, Collectors.counting()));
            
            logger.warn("Recent error breakdown: {}", recentErrorTypes);
        }
    }
    
    public XMLErrorReport generateReport() {
        Map<XMLErrorType, Long> errorCounts = errorCountsByType.entrySet().stream()
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                entry -> entry.getValue().get()
            ));
        
        return new XMLErrorReport(
            totalErrors.get(),
            errorCounts,
            new ArrayList<>(recentErrors.getItems()),
            Instant.now()
        );
    }
}

Best Practices Summary

Error Handling Guidelines

Do:

  • Use appropriate error handlers for different parsing methods
  • Implement graceful degradation strategies
  • Provide detailed error messages with context
  • Log errors appropriately for debugging
  • Validate input before processing
  • Set timeouts for parsing operations
  • Monitor error rates and patterns

Don't:

  • Ignore parsing errors or warnings
  • Expose sensitive information in error messages
  • Fail entire operations for minor validation errors
  • Use generic exception handling without specificity
  • Process XML without proper error handling
  • Log excessive detail in production environments

Error Recovery Checklist

  • [ ] Custom error handlers implemented
  • [ ] Input validation before processing
  • [ ] Graceful handling of malformed XML
  • [ ] Recovery strategies for common errors
  • [ ] Appropriate logging levels configured
  • [ ] Error metrics and monitoring in place
  • [ ] User-friendly error messages
  • [ ] Security considerations in error handling
  • [ ] Performance impact of error handling minimized
  • [ ] Regular testing of error scenarios

Additional Resources

Oracle XML Error Handling

SAX Error Handler Documentation

StAX Exception Handling