koala73/worldmonitor · warning · ValidationError
year must be 0 or between 1951 and ${currentYear}
Error message
year must be 0 or between 1951 and ${currentYear} What it means
getDisplacementSummary requires req.year to be an integer that is either 0 (meaning 'latest') or within the supported UNHCR range 1951..currentYear. Anything else (floats, strings pre-coercion, years outside the range) throws a ValidationError carrying the field 'year' and the dynamic range message.
Solutions
- Clamp client-side: year = Math.round(year) and reject <1951 or >new Date().getFullYear(), allowing 0
- Send 0 when you want the latest data instead of new Date().getFullYear()
- Catch ValidationError and read the field/description array to prompt the user for a valid year
Example fix
// before
await getDisplacementSummary(ctx, { year: 1949 });
// after
const year = requested < 1951 ? 0 : requested; // 0 = latest
await getDisplacementSummary(ctx, { year }); Defensive patterns
Strategy: validation
Validate before calling
const y = req.year;
const valid = Number.isInteger(y) && (y === 0 || (y >= 1951 && y <= new Date().getFullYear()));
if (!valid) throw new Error(`year must be 0 or between 1951 and ${new Date().getFullYear()}`); Type guard
const isValidYear = (v: unknown): v is number => typeof v === 'number' && Number.isInteger(v) && (v === 0 || (v >= 1951 && v <= new Date().getFullYear()));
Try / catch
try {
return await getDisplacementSummary(ctx, { year });
} catch (e) {
if (e instanceof ValidationError && e.issues.some(i => i.field === 'year')) {
return await getDisplacementSummary(ctx, { year: 0 }); // fall back to latest
}
throw e;
} Prevention
- Remember 0 is the sentinel for 'latest year'
- Clamp user-selected years into [1951, currentYear]
- Send year as a number, never a string
When it happens
Trigger: Passing year=1949 (before UNHCR reporting starts), year=2077 (beyond currentYear), 2023.5, or a non-integer; client sending the year as a string when the schema expects a number.
Common situations: Hardcoded historical-year presets predating 1951; date pickers producing fractional epochs; forgetting that 0 is the sentinel for 'current year' and blocking it client-side.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- digestHour must be an integer 0–23
- quietHoursStart must be an integer 0–23
- quietHoursEnd must be an integer 0–23
- ${label} must be between 0 and 1 million with at most 6 deci
- ${label} HTTP 400
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/0a32951f63946e25.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/displacement/v1/get-displacement-summary.ts:16
import type {
ServerContext,
GetDisplacementSummaryRequest,
GetDisplacementSummaryResponse,
} from '../../../../src/generated/server/worldmonitor/displacement/v1/service_server';
import { ValidationError } from '../../../../src/generated/server/worldmonitor/displacement/v1/service_server';
import { getCachedJson } from '../../../_shared/redis';
// Railway owns fetching, aggregation and publication; RPC callers only select seed data.
export async function getDisplacementSummary(
_ctx: ServerContext,
req: GetDisplacementSummaryRequest,
): Promise<GetDisplacementSummaryResponse> {
const currentYear = new Date().getFullYear();
if (!Number.isInteger(req.year) || (req.year !== 0 && (req.year < 1951 || req.year > currentYear))) {
throw new ValidationError([{ field: 'year', description: `year must be 0 or between 1951 and ${currentYear}` }]);
}
const emptyResponse: GetDisplacementSummaryResponse = {
summary: {
year: req.year || currentYear,
globalTotals: { refugees: 0, asylumSeekers: 0, idps: 0, stateless: 0, total: 0 },
countries: [],
topFlows: [],
},
fetchedAt: 0,
dataAvailable: false,
};
try {
// The current-year key can contain prior-year data when UNHCR has not published yet.
const [seedData, seedMeta] = await Promise.all([
getCachedJson(`displacement:summary:v1:${currentYear}`, true) as Promise<GetDisplacementSummaryResponse | null>,
getCachedJson('seed-meta:displacement:summary', true) as Promise<{ fetchedAt?: number } | null>,
]);View on GitHub (pinned to 7d06c8633d)