Error Handling & Conformance
Master error handling strategies and conformance modes to build robust PDF generation systems that gracefully handle invalid input and unexpected situations.
Learning Objectives
By the end of this article, you’ll be able to:
- Implement proper error handling
- Choose between Strict and Lax conformance modes
- Handle parsing and rendering errors
- Validate input before generation
- Build fault-tolerant systems
- Recover from common errors
Conformance Modes
Scryber provides two conformance modes for HTML/CSS parsing:
Strict Mode
using Scryber.Components;
using Scryber.PDF;
var doc = Document.ParseDocument("template.html");
doc.ConformanceMode = ParserConformanceMode.Strict;
// Strict mode behavior:
// - Fails on invalid HTML/CSS
// - Throws exceptions for errors
// - Ensures standards compliance
// - Best for controlled environments
Use Strict Mode when:
- Templates are controlled by your team
- You want to catch errors early
- Standards compliance is critical
- Testing and validation
Lax Mode
var doc = Document.ParseDocument("template.html");
doc.ConformanceMode = ParserConformanceMode.Lax;
// Lax mode behavior:
// - Attempts to recover from errors
// - Logs warnings instead of failing
// - Skips invalid elements
// - Best for user-generated content
Use Lax Mode when:
- Handling user-generated HTML
- Graceful degradation needed
- Can’t guarantee input quality
- Production systems
Basic Error Handling
Try-Catch Pattern
using Scryber.Components;
using Scryber.PDF;
using System;
using System.IO;
public void GeneratePDF(string templatePath, Stream output)
{
try
{
var doc = Document.ParseDocument(templatePath);
doc.SaveAsPDF(output);
Console.WriteLine("PDF generated successfully");
}
catch (PDFException ex)
{
// Scryber-specific errors
Console.WriteLine($"PDF Generation Error: {ex.Message}");
throw;
}
catch (FileNotFoundException ex)
{
// File errors
Console.WriteLine($"Template not found: {ex.FileName}");
throw;
}
catch (Exception ex)
{
// Other errors
Console.WriteLine($"Unexpected error: {ex.Message}");
throw;
}
}
With Logging
using Scryber.Logging;
public void GeneratePDFWithLogging(string templatePath, Stream output)
{
var logger = new PDFCollectorTraceLog(TraceRecordLevel.Verbose);
try
{
var doc = Document.ParseDocument(templatePath);
doc.TraceLog = logger;
doc.SaveAsPDF(output);
}
catch (Exception ex)
{
// Log includes diagnostic information
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine("\nDiagnostic Log:");
foreach (var entry in logger.Entries)
{
Console.WriteLine($"[{entry.Level}] {entry.Message}");
}
throw;
}
}
Conformance-Based Error Handling
Strict Mode with Validation
public void GenerateStrictPDF(string templatePath, Stream output)
{
var logger = new PDFCollectorTraceLog(TraceRecordLevel.Verbose);
try
{
var doc = Document.ParseDocument(templatePath);
doc.ConformanceMode = ParserConformanceMode.Strict;
doc.TraceLog = logger;
doc.SaveAsPDF(output);
}
catch (PDFException ex)
{
// Document has invalid HTML/CSS
Console.WriteLine("Document validation failed:");
Console.WriteLine(ex.Message);
// Show specific errors from log
var errors = logger.Entries
.Where(e => e.Level == TraceRecordLevel.Errors)
.ToList();
foreach (var error in errors)
{
Console.WriteLine($" - {error.Category}: {error.Message}");
}
throw;
}
}
Lax Mode with Warning Checks
public bool GenerateLaxPDF(string templatePath, Stream output)
{
var logger = new PDFCollectorTraceLog(TraceRecordLevel.Warnings);
try
{
var doc = Document.ParseDocument(templatePath);
doc.ConformanceMode = ParserConformanceMode.Lax;
doc.TraceLog = logger;
doc.SaveAsPDF(output);
// Check for warnings
var warnings = logger.Entries
.Where(e => e.Level == TraceRecordLevel.Warnings)
.ToList();
if (warnings.Any())
{
Console.WriteLine($"PDF generated with {warnings.Count} warnings:");
foreach (var warning in warnings.Take(5))
{
Console.WriteLine($" - {warning.Message}");
}
// Return false to indicate issues (but PDF was generated)
return false;
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"PDF generation failed: {ex.Message}");
throw;
}
}
Input Validation
Pre-Generation Validation
public class DocumentValidator
{
public ValidationResult ValidateTemplate(string templatePath)
{
var result = new ValidationResult();
// Check file exists
if (!File.Exists(templatePath))
{
result.Errors.Add($"Template file not found: {templatePath}");
return result;
}
// Check file is readable
try
{
var content = File.ReadAllText(templatePath);
// Basic HTML validation
if (!content.Contains("<html") && !content.Contains("<HTML"))
{
result.Warnings.Add("Template may not be valid HTML");
}
// Check for required elements
if (!content.Contains("<body") && !content.Contains("<BODY"))
{
result.Warnings.Add("Template missing body element");
}
}
catch (Exception ex)
{
result.Errors.Add($"Cannot read template: {ex.Message}");
return result;
}
result.IsValid = !result.Errors.Any();
return result;
}
public ValidationResult ValidateData(object data)
{
var result = new ValidationResult();
if (data == null)
{
result.Errors.Add("Data cannot be null");
result.IsValid = false;
return result;
}
// Add specific data validation logic here
result.IsValid = !result.Errors.Any();
return result;
}
}
public class ValidationResult
{
public bool IsValid { get; set; }
public List<string> Errors { get; set; } = new List<string>();
public List<string> Warnings { get; set; } = new List<string>();
}
// Usage
var validator = new DocumentValidator();
var validationResult = validator.ValidateTemplate("template.html");
if (!validationResult.IsValid)
{
Console.WriteLine("Validation failed:");
foreach (var error in validationResult.Errors)
{
Console.WriteLine($" - {error}");
}
return;
}
// Proceed with generation
GeneratePDF("template.html", output);
Practical Examples
Example 1: Robust Invoice Generator
using Scryber.Components;
using Scryber.Logging;
using Scryber.PDF;
using System;
using System.IO;
using System.Linq;
public class RobustInvoiceGenerator
{
private readonly string _templatePath;
private readonly ILogger _logger;
public RobustInvoiceGenerator(string templatePath, ILogger logger)
{
_templatePath = templatePath;
_logger = logger;
}
public GenerationResult Generate(Invoice invoice, Stream output)
{
var result = new GenerationResult { InvoiceNumber = invoice.Number };
var scryberLogger = new PDFCollectorTraceLog(TraceRecordLevel.Warnings);
try
{
// Validate input
ValidateInvoice(invoice);
// Load and configure document
var doc = Document.ParseDocument(_templatePath);
doc.ConformanceMode = ParserConformanceMode.Lax; // Graceful degradation
doc.TraceLog = scryberLogger;
// Set metadata
doc.Info.Title = $"Invoice {invoice.Number}";
doc.Info.Author = invoice.CompanyName;
// Bind data
doc.Params["invoice"] = invoice;
// Generate
doc.SaveAsPDF(output);
// Check for warnings
var warnings = scryberLogger.Entries
.Where(e => e.Level == TraceRecordLevel.Warnings)
.Select(e => e.Message)
.ToList();
if (warnings.Any())
{
result.Warnings.AddRange(warnings);
_logger.LogWarning($"Invoice {invoice.Number} generated with {warnings.Count} warnings");
}
result.Success = true;
result.Message = "Invoice generated successfully";
}
catch (ArgumentException ex)
{
// Validation error
result.Success = false;
result.Message = $"Invalid invoice data: {ex.Message}";
_logger.LogError(ex, $"Validation failed for invoice {invoice.Number}");
}
catch (PDFException ex)
{
// PDF generation error
result.Success = false;
result.Message = $"PDF generation failed: {ex.Message}";
result.LogEntries.AddRange(scryberLogger.Entries.Select(e => e.Message));
_logger.LogError(ex, $"PDF generation failed for invoice {invoice.Number}");
}
catch (Exception ex)
{
// Unexpected error
result.Success = false;
result.Message = $"Unexpected error: {ex.Message}";
_logger.LogError(ex, $"Unexpected error generating invoice {invoice.Number}");
}
return result;
}
private void ValidateInvoice(Invoice invoice)
{
if (string.IsNullOrWhiteSpace(invoice.Number))
throw new ArgumentException("Invoice number is required");
if (string.IsNullOrWhiteSpace(invoice.CompanyName))
throw new ArgumentException("Company name is required");
if (invoice.Items == null || !invoice.Items.Any())
throw new ArgumentException("Invoice must have at least one item");
if (invoice.Total <= 0)
throw new ArgumentException("Invoice total must be positive");
}
}
public class GenerationResult
{
public bool Success { get; set; }
public string Message { get; set; }
public string InvoiceNumber { get; set; }
public List<string> Warnings { get; set; } = new List<string>();
public List<string> LogEntries { get; set; } = new List<string>();
}
// Usage
var generator = new RobustInvoiceGenerator("invoice-template.html", logger);
using (var output = new FileStream("invoice.pdf", FileMode.Create))
{
var result = generator.Generate(invoice, output);
if (result.Success)
{
Console.WriteLine($"✓ {result.Message}");
if (result.Warnings.Any())
{
Console.WriteLine($" {result.Warnings.Count} warnings (see logs)");
}
}
else
{
Console.WriteLine($"✗ {result.Message}");
if (result.LogEntries.Any())
{
Console.WriteLine(" Diagnostic information:");
foreach (var entry in result.LogEntries.Take(10))
{
Console.WriteLine($" - {entry}");
}
}
}
}
Example 2: User-Generated Content Handler
public class UserContentHandler
{
public PDFGenerationResult GenerateFromUserContent(
string userHtml,
object userData,
Stream output)
{
var result = new PDFGenerationResult();
var logger = new PDFCollectorTraceLog(TraceRecordLevel.Warnings);
try
{
// Sanitize user HTML (basic example - use proper sanitization library)
var sanitizedHtml = SanitizeHtml(userHtml);
// Create document from string
var doc = Document.ParseDocument(new StringReader(sanitizedHtml));
// ALWAYS use Lax mode for user content
doc.ConformanceMode = ParserConformanceMode.Lax;
doc.TraceLog = logger;
// Bind user data
doc.Params["data"] = userData;
// Generate
doc.SaveAsPDF(output);
// Collect warnings for user feedback
result.Success = true;
result.Warnings = logger.Entries
.Where(e => e.Level == TraceRecordLevel.Warnings)
.Select(e => new UserFriendlyWarning
{
Message = SimplifyWarningMessage(e.Message),
Category = e.Category
})
.ToList();
if (result.Warnings.Any())
{
result.Message = $"PDF generated with {result.Warnings.Count} issues";
}
else
{
result.Message = "PDF generated successfully";
}
}
catch (Exception ex)
{
result.Success = false;
result.Message = "Failed to generate PDF from provided content";
result.TechnicalDetails = ex.Message;
}
return result;
}
private string SanitizeHtml(string html)
{
// Use HtmlSanitizer or similar library in production
// This is a simplified example
return html;
}
private string SimplifyWarningMessage(string technicalMessage)
{
// Convert technical messages to user-friendly ones
if (technicalMessage.Contains("font not found"))
return "Some text may not display with the requested font";
if (technicalMessage.Contains("image not found"))
return "Some images could not be loaded";
if (technicalMessage.Contains("invalid CSS"))
return "Some styling may not be applied correctly";
return "Minor issue detected (document still generated)";
}
}
public class PDFGenerationResult
{
public bool Success { get; set; }
public string Message { get; set; }
public string TechnicalDetails { get; set; }
public List<UserFriendlyWarning> Warnings { get; set; } = new List<UserFriendlyWarning>();
}
public class UserFriendlyWarning
{
public string Message { get; set; }
public string Category { get; set; }
}
Try It Yourself
Exercise 1: Validation Framework
Build a validation framework that:
- Checks template exists
- Validates HTML structure
- Verifies required data fields
- Returns detailed validation results
- Tests with various inputs
Exercise 2: Error Recovery
Implement error recovery that:
- Catches specific exception types
- Logs detailed error information
- Attempts alternative approaches
- Provides fallback behavior
- Tests with invalid inputs
Exercise 3: Conformance Comparison
Create a comparison tool that:
- Generates same document in Strict and Lax modes
- Compares results
- Shows differences in error handling
- Documents best practices
Common Pitfalls
❌ No Error Handling
// Fails silently or crashes
var doc = Document.ParseDocument("template.html");
doc.SaveAsPDF(stream);
✅ Solution:
try
{
var doc = Document.ParseDocument("template.html");
doc.SaveAsPDF(stream);
}
catch (PDFException ex)
{
Logger.LogError($"PDF generation failed: {ex.Message}");
throw;
}
❌ Wrong Conformance Mode
// Strict mode with user-generated content - will fail often
doc.ConformanceMode = ParserConformanceMode.Strict;
✅ Solution:
// Lax mode for user content
doc.ConformanceMode = ParserConformanceMode.Lax;
❌ Ignoring Warnings
// Warnings ignored - issues not addressed
var logger = new PDFCollectorTraceLog(TraceRecordLevel.Warnings);
doc.TraceLog = logger;
doc.SaveAsPDF(stream);
// Never check logger.Entries
✅ Solution:
var logger = new PDFCollectorTraceLog(TraceRecordLevel.Warnings);
doc.TraceLog = logger;
doc.SaveAsPDF(stream);
// Check and report warnings
if (logger.Entries.Any(e => e.Level == TraceRecordLevel.Warnings))
{
ReportWarnings(logger.Entries);
}
Error Handling Checklist
- Try-catch blocks around PDF generation
- Specific exception handling (PDFException, FileNotFoundException)
- Logging enabled for diagnostics
- Appropriate conformance mode chosen
- Input validation before generation
- Warnings checked and reported
- Error messages user-friendly
- Recovery strategies implemented
Best Practices
- Always Use Try-Catch - Never assume success
- Lax Mode for User Content - Graceful degradation
- Strict Mode for Testing - Catch issues early
- Log Everything - Diagnostic information crucial
- Validate Input - Before attempting generation
- Check Warnings - Even in Lax mode
- User-Friendly Messages - Translate technical errors
- Test Error Paths - Verify error handling works
Next Steps
- PDF Versions - Version and compliance settings
- Security - Protect your documents
- Production & Deployment - Production best practices
Continue learning → PDF Versions