Back to KB
Difficulty
Intermediate
Read Time
9 min

How to build a grid emission factor lookup in Vanilla JS using IEA 2026 regional data

By Codcompass Team··9 min read

Deterministic Carbon Accounting: Building a Subregional Grid Intensity Lookup for Scope 2 Compliance

Current Situation Analysis

Regulatory frameworks like the CSRD, SEC climate disclosure rules, and the GHG Protocol Corporate Standard have shifted carbon accounting from a voluntary sustainability exercise to a strict financial compliance requirement. At the center of this shift is Scope 2 emissions reporting, which covers indirect greenhouse gas emissions from purchased electricity, steam, heating, and cooling.

The industry pain point is not a lack of data; it is a lack of deterministic, auditable calculation pipelines. Most commercial carbon SDKs and internal tooling fall into two failing patterns:

  1. Global/National Averaging: Calculators apply a single country-level emission factor to all consumption within that jurisdiction. This approach ignores grid topology, generation mix variations, and regional policy impacts.
  2. External API Dependency: Teams outsource factor resolution to third-party carbon data providers. This introduces network latency, rate-limiting costs, and non-deterministic behavior. If the API returns a cached or outdated value, the calculation chain breaks silently.

Both patterns violate the core requirement of regulatory reporting: reproducibility. Auditors expect identical inputs to yield identical outputs, with full traceability to the authoritative source.

The misunderstanding stems from treating grid intensity as a static scalar. In reality, electricity grids are highly fragmented. The GHG Protocol explicitly mandates dual reporting: location-based (average grid intensity where consumption occurs) and market-based (intensity from specific contracts, PPAs, or energy attribute certificates). Location-based calculations must reflect the actual grid topology, not a political boundary average.

Data confirms the material impact of this oversight. According to EPA eGRID 2024, the United States national average grid intensity sits at 0.386 kg CO₂e/kWh. However, the NYUP subregion (Upstate New York), dominated by hydroelectric and nuclear generation, operates at 0.125 kg CO₂e/kWh. Applying the national average to a NYUP data center inflates reported Scope 2 emissions by approximately 200%. Similarly, DEFRA’s 2025 conversion factors show the UK grid intensity dropped 15% year-over-year (0.207 → 0.177 kg CO₂e/kWh). Calculators that hardcode previous-year values generate phantom emissions that distort baseline tracking and compliance filings.

WOW Moment: Key Findings

The following comparison isolates the operational and compliance trade-offs between common implementation strategies. The metrics reflect production deployment characteristics, not theoretical benchmarks.

ApproachGranularityCompliance RiskRuntime OverheadAudit Trail Quality
Global Average (0.475 kg/kWh)Country/NoneCriticalZeroNone
National AverageCountryHighZeroWeak
External API LookupSubregionalMediumHigh (Network + Latency)Fragmented
Deterministic Subregional RegistrySubregionalLowZeroFull (Inline Citation)

Why this matters: A deterministic subregional registry eliminates network dependencies while preserving regulatory granularity. By embedding source metadata directly into the calculation payload, you enforce auditability at the code level. This approach reduces calculation latency to sub-millisecond levels, guarantees identical outputs across environments, and satisfies GHG Protocol location-based requirements without third-party vendor lock-in.

Core Solution

Building a compliant, zero-dependency grid intensity lookup requires three architectural decisions: data topology, state injection, and citation enforcement. The implementation below uses TypeScript to enforce type safety, explicit null handling, and structured audit payloads.

Step 1: Define the Data Contract

Grid intensity records must carry more than a numeric factor. They require source attribution, publication year, geographic scope, and optional contextual notes. This metadata travels with the calculation result, ensuring downstream systems never display a number without its provenance.

interface GridIntensityRecord {
  factor: number;
  unit: 'kg CO2e per kWh';
  source: 'IEA_2026' | 'DEFRA_2025' | 'EPA_2024';
  year: number;
  regionName: string;
  subregionCode?: string;
  generationMixNote?: string;
}

type GridRegistry = Record<string, GridIntensityRecord>;

Step 2: Construct the Lookup Registry

Store factors in a flat, key-value structure. Keys follow ISO 3166-1 alpha-2 for countries and EPA eGRID subregion codes for US states. A flat map guarantees O(1) resolution time and eliminates nested traversal logic.

const GRID_INTENSITY_REGISTRY: GridRegistry = {
  // European Union & UK
  GB: {
    factor: 0.177,
    unit: 'kg CO2e per kWh',
    source: 'DEFRA_2025',
    year: 2025,
    regionName: 'United Kingdom',
    generationMixNote: 'DEFRA 2025. -15% vs 2024 baseline.'
  },
  DE: {
    factor: 0.364,
    unit: 'kg CO2e per kWh',
    source: 'IEA_2026',
    year: 2026,
    regionName: 'Germany'
  },
  FR: {
    factor: 0.052,
    unit: 'kg CO2e per kWh',
    source: 'IEA_2026',
    year: 2026,
    regionName: 'France',
    generationMixNote: '~70% nuclear generation'
  },

  // Asia-Pacific
  IN: {
    factor: 0.708,
    unit: 'kg CO2e per kWh',
    source: 'IEA_2026',
    year: 2026,
    regionName: 'India'
  },
  CN: {
    factor: 0.581,
    unit: 'kg CO2e per kWh',
    source: 'IEA_2026',
    year: 2026,
    regionName: 'China',
    generationMixNote: 'Declining intensity as solar/wind scales'
  },

  // US Subregions (EPA eGRID 2024)
  US_NATIONAL: {
    factor: 0.386,
    unit: 'kg CO2e per kWh',
    source: 'EPA_2024',
    year: 2024,
    regionName: 'United States (National Average)'
  },
  US_NYUP: {
    factor: 0.1249,
    unit: 'kg CO2e per kWh',
    source: 'EPA_2024',
    year: 2024,
    regionName: 'NYUP — Upstate New York',
    generationMixNote: 'Hydro 31% + Nuclear 31%'
  },
  US_WECC_CAMX: {
    factor: 0.2265,
    unit: 'kg CO2e per kWh',
    source: 'EPA_2024',
    year: 2024,
    regionName: 'CAMX — California',
    generationMixNote: 'Gas 46% + Solar 20%'
  },
  US_ERCT: {
    factor: 0.3512,
    unit: 'kg CO2e per kWh',
    source: 'EPA_2024',
    year: 2024,
    regionName: 'ERCT — Texas (ERCOT)',
    generationMixNote: 'Gas 47% + Wind 23%'
  },
  US_RFCW: {
    factor: 0.4563,
    unit: 'kg CO2e per kWh',
    source: 'EPA_2024',
    year: 2024,
    regionName: 'RFCW — Ohio Valley',
    generationMixNote: 'Coal 31% + Gas 32%'
  },
  US_SRMW: {
    factor: 0.6260,
    unit: 'kg CO2e per kWh',
 

source: 'EPA_2024', year: 2024, regionName: 'SRMW — SERC Midwest', generationMixNote: 'Coal 59% — Highest subregion intensity' } } as const;


### Step 3: Implement the Resolution Engine

The lookup function must fail explicitly. Returning `undefined` or `NaN` corrupts downstream financial models. Use a type guard to validate the registry state before resolution.

```typescript
type ResolutionResult = GridIntensityRecord | null;

function resolveGridIntensity(
  regionKey: string,
  registry: GridRegistry
): ResolutionResult {
  const normalizedKey = regionKey.trim().toUpperCase();
  const record = registry[normalizedKey];

  if (!record) {
    console.warn(
      `[CarbonEngine] No grid intensity record found for key: "${normalizedKey}". ` +
      `Falling back to null to prevent silent NaN propagation.`
    );
    return null;
  }

  return record;
}

Step 4: Build the Calculation & Citation Payload

Scope 2 location-based emissions require precise unit conversion and mandatory source attribution. The function returns a structured report object that downstream UI or API layers can render without additional formatting logic.

interface Scope2LocationReport {
  consumptionKwh: number;
  intensityFactor: number;
  emissionsKg: number;
  emissionsTonnes: number;
  citation: string;
  generationContext?: string;
  dataVersion: string;
}

function computeLocationBasedEmissions(
  consumptionKwh: number,
  regionKey: string,
  registry: GridRegistry,
  dataVersion: string
): Scope2LocationReport | null {
  if (consumptionKwh < 0 || !Number.isFinite(consumptionKwh)) {
    throw new Error('[CarbonEngine] Invalid consumption value. Must be a non-negative finite number.');
  }

  const record = resolveGridIntensity(regionKey, registry);
  if (!record) return null;

  const emissionsKg = consumptionKwh * record.factor;

  return {
    consumptionKwh,
    intensityFactor: record.factor,
    emissionsKg: parseFloat(emissionsKg.toFixed(4)),
    emissionsTonnes: parseFloat((emissionsKg / 1000).toFixed(6)),
    citation: `${record.source} (${record.year}) — ${record.regionName}`,
    generationContext: record.generationMixNote,
    dataVersion
  };
}

Architecture Rationale

  • Flat Registry over Nested Objects: O(1) key resolution eliminates traversal overhead. Carbon calculations often run in tight loops (e.g., processing thousands of meter readings). Nested structures introduce unnecessary cognitive and computational complexity.
  • Server-Side Injection: The registry should be serialized into the initial HTML payload or injected via a build-time constant. This guarantees deterministic state across client, server, and edge runtimes. No network fetches, no race conditions.
  • Explicit Citation Enforcement: By bundling citation and dataVersion directly into the return payload, you prevent UI layers from displaying unattributed numbers. Auditors require source traceability at the point of display.
  • Strict Type Guards: Returning null on missing keys forces the calling code to handle the absence of data explicitly. This prevents NaN * 0 = 0 bugs that silently zero out emissions in financial reports.

Pitfall Guide

1. Conflating Location-Based and Market-Based Methods

Explanation: The GHG Protocol requires separate reporting for location-based (grid average) and market-based (contract-specific) emissions. Mixing them in a single calculation field violates disclosure standards. Fix: Maintain separate calculation pipelines. Location-based uses the deterministic registry. Market-based requires user-supplied contract data, EAC/REGO certificates, or supplier-specific factors. Never average them.

2. Relying on National Averages for Site-Specific Calculations

Explanation: National averages mask subregional generation mix variations. A data center in a hydro-heavy region will appear artificially carbon-intensive if calculated against a coal-heavy national average. Fix: Always resolve to the most granular subregion available. Use EPA eGRID subregions for the US, NUTS-2/3 for Europe, and state/province codes elsewhere. Fall back to national only when subregional data is genuinely unavailable.

3. Silent NaN Propagation in Calculation Chains

Explanation: Missing registry keys or malformed inputs produce NaN. JavaScript arithmetic with NaN propagates silently, corrupting downstream aggregations and financial totals. Fix: Validate inputs with Number.isFinite(). Return null on missing data. Force calling code to handle the null case before aggregation. Use TypeScript strict mode to catch unhandled nulls at compile time.

4. Omitting Source Citations from Output Payloads

Explanation: Displaying emission totals without source attribution violates audit requirements. Regulators will reject filings that cannot trace numbers back to authoritative publications. Fix: Bundle citation, source, year, and dataVersion into every calculation result. Render citations inline in the UI, not hidden in tooltips or footnotes.

5. Hardcoding Version Numbers Without CI Validation

Explanation: Grid factors update annually. Hardcoding a version string without automated validation leads to stale data deployments. A mismatch between the registry and the declared version creates compliance drift. Fix: Store the version as a constant. Add a CI step that compares the registry's internal year metadata against the declared version. Fail the build if they diverge. Log a warning on every page load if a mismatch is detected at runtime.

6. Ignoring Unit Conversion Standards

Explanation: Emission factors are typically in kg CO₂e/kWh, but regulatory filings often require tonnes (tCO₂e). Manual division introduces rounding errors and inconsistent precision across reports. Fix: Standardize precision in the calculation layer. Use toFixed(4) for kg and toFixed(6) for tonnes. Parse back to numbers with parseFloat() to avoid string concatenation bugs in downstream math.

7. Assuming Grid Factors Are Static Post-Deployment

Explanation: IEA publishes in April, DEFRA in June, EPA eGRID in January. Factors change annually. Treating the registry as immutable leads to outdated disclosures. Fix: Implement a versioned update workflow. Tag each registry entry with its publication year. Schedule quarterly reviews aligned with source publication calendars. Automate diff checks between old and new factor sets to highlight material changes (>5% variance).

Production Bundle

Action Checklist

  • Source Validation: Verify all factors against IEA 2026, DEFRA 2025, and EPA eGRID 2024 official publications before ingestion.
  • Registry Structure: Flatten all factors into a single key-value map with explicit source, year, and citation fields.
  • Type Safety: Implement TypeScript interfaces for records, resolution results, and emission reports. Enforce strict null checks.
  • Injection Strategy: Serialize the registry into the initial payload or build-time constant. Eliminate runtime fetches.
  • Citation Enforcement: Bundle source attribution directly into calculation payloads. Render inline in all UI outputs.
  • Version Control: Tag the registry with a version constant. Add CI validation to prevent stale deployments.
  • Update Calendar: Hardcode publication schedules (IEA April, DEFRA June, EPA January) as source comments. Schedule quarterly reviews.

Decision Matrix

ScenarioRecommended ApproachWhyCost Impact
Startup MVP / Internal DashboardNational Average RegistryFastest implementation, covers 80% of use cases, low maintenance overhead$0 (Zero dependencies)
Enterprise CSRD/SEC FilingSubregional Deterministic RegistryMeets GHG Protocol granularity, eliminates audit risk, ensures reproducibility$0 (Zero dependencies)
Real-Time IoT MeteringEdge-Injected Registry + Local CacheSub-millisecond resolution, works offline, prevents network bottlenecks$0 (Zero dependencies)
Market-Based Contract TrackingExternal API + Manual Override LayerRequires dynamic contract data, PPAs, and EAC certificates not available in static grids$50-$200/mo (API fees)

Configuration Template

// carbon-registry.config.ts
export const GRID_DATA_VERSION = '2025.6';

export const GRID_INTENSITY_REGISTRY = {
  // Add entries following the GridIntensityRecord interface
  // Ensure source, year, and regionName are always present
} as const;

export const GRID_UPDATE_CALENDAR = {
  IEA: 'April (Global Energy Review)',
  DEFRA: 'June (GHG Conversion Factors)',
  EPA: 'January (eGRID Summary Tables)'
} as const;

// CI Validation Hook (pseudo-code)
export function validateRegistryVersion(registry: typeof GRID_INTENSITY_REGISTRY, declaredVersion: string): boolean {
  const years = new Set(Object.values(registry).map(r => r.year));
  const hasMismatch = Array.from(years).some(y => y.toString() !== declaredVersion.split('.')[0]);
  if (hasMismatch) {
    console.error(`[Registry] Version mismatch detected. Declared: ${declaredVersion}, Found years: ${Array.from(years)}`);
  }
  return !hasMismatch;
}

Quick Start Guide

  1. Initialize the Registry: Copy the configuration template into your project. Populate the GRID_INTENSITY_REGISTRY with your target regions using the official IEA, DEFRA, and EPA datasets.
  2. Inject State: Serialize the registry into your application's initial state. For web apps, inject it via a <script> tag or build-time constant. For Node.js, import it directly.
  3. Run Resolution: Call resolveGridIntensity(regionKey, registry) to fetch the factor. Handle null returns explicitly to prevent calculation corruption.
  4. Compute & Report: Pass consumption data and the resolved record into computeLocationBasedEmissions(). Extract the citation and emissionsTonnes fields for display or API transmission.
  5. Validate Deployment: Run the CI version check before merging. Ensure the declared GRID_DATA_VERSION matches the publication years in the registry. Deploy with zero network dependencies.