Home › Database › ORA-01843 Not a Valid Month — Fix Date Masks
Beginner 5 min · October 27, 2026

ORA-01843 Not a Valid Month — Fix Date Masks

Fix ORA-01843 by replacing implicit string-to-date conversion with TO_DATE and an explicit mask.

N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.

Follow
✓ Production
production tested
October 27, 2026
last updated
2,087
articles · all by Naren
Before you start⏱ 9 min
  • ✓Basic programming fundamentals (variables, functions, control flow)
  • ✓No prior database experience needed — we start from first principles
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • ORA-01843 means Oracle can't resolve a month token — usually a string converted under the wrong NLS_DATE_FORMAT
  • Never rely on implicit conversion: wrap every string in TO_DATE with an explicit mask like 'DD/MM/YYYY'
  • DD/MM versus MM/DD decides whether '03/04' is March or April — the mask settles it, not the data
  • Diff NLS_SESSION_PARAMETERS between dev and prod, then store DATE columns and pass bind variables
Plain-English First

Think of Oracle as a strict librarian who files dates by a card system. Hand her 03/04/2026 and she checks her local rulebook: in one branch that's March 4, in another it's April 3, and 13/04/2026 matches no card at all — rejected, that's ORA-01843. The fix is writing the rule on the card itself with TO_DATE, or better, handing her pre-sorted date cards (bind variables) so the branch rulebook never matters.

ORA-01843: not a valid month. It strikes at the worst hour — a month-end batch, a payroll run, a migration replaying a million rows — and the message feels insulting: of course April is a valid month. But Oracle isn't judging the calendar; it's saying the month token in your string doesn't parse under the format it assumed. Somewhere between your literal and the DATE column, the format got lost.

The trap is implicit conversion. Write WHERE hire_date > '03/04/2026' and Oracle silently converts the string using NLS_DATE_FORMAT, a session setting you didn't choose and probably haven't checked. On your laptop that setting reads DD/MM/YYYY and everything works. In production the same session reads MM/DD/YYYY, '13/04/2026' becomes month 13, and the batch dies at 1:40 AM on row 400,000. Same code, same data family, different dictionary.

This guide makes the conversion explicit: reproducing 01843 on demand, reading NLS_SESSION_PARAMETERS like a detective, picking the right TO_DATE mask (and the right century rule), defusing DD/MM versus MM/DD, and hardening the pipeline with DATE literals, binds, and staging validation so month-end stops being scary.

What ORA-01843 Actually Rejects

ORA-01843 fires when the month piece of a date string can't resolve. Three flavors exist: a number above 12 ('13/04/2026' under MM/DD), a month name in the wrong session language ('JANV' when NLS_DATE_LANGUAGE is English), and pure garbage where a month should be ('XX/04/2026'). Oracle reports the month because it's the first token the parser chokes on — the day and year may be equally wrong, but you'll never hear about them until the month parses.

The cruel variant is the silent swap. '03/04/2026' parses fine under both DD/MM and MM/DD, producing April 3 in one session and March 4 in another. No error, no warning, just wrong dates flowing into payslips, expiry calculations, and regulatory reports. Teams discover this months later during reconciliation, when the error that would have saved them never fired because both readings were technically valid.

Treat every 01843 as a question about the assumed format, not about the calendar. Ask which mask the session applied, whether the data matches that mask, and which rows would parse differently under the other mask. The answers below give you the probes; the habit of asking saves you the incident.

reproduce_01843.sqlSQL
1
2
3
4
5
6
7
8
9
-- Which mask does the data obey? One of these parses, one explodes
SELECT TO_DATE('03/04/2026', 'DD/MM/YYYY') AS dd_mm FROM DUAL;
SELECT TO_DATE('03/04/2026', 'MM/DD/YYYY') AS mm_dd FROM DUAL;

-- Reproduces ORA-01843 on demand: month 13 cannot exist
-- SELECT TO_DATE('13/04/2026', 'MM/DD/YYYY') FROM DUAL;

-- Month names obey the session language, numbers don't
SELECT TO_DATE('03-APR-2026', 'DD-MON-YYYY') FROM DUAL;
📊 Production Insight
A reconciliation found 11,000 payslip dates off by swapped months — every one had parsed cleanly under the wrong mask, so no error ever fired.
🎯 Key Takeaway
01843 is a failed month token — but the silent month-swap on ambiguous dates is the costlier bug.

Implicit Conversion Is the Loaded Gun

Implicit conversion is Oracle converting a string to a DATE without being asked, using NLS_DATE_FORMAT as the assumed mask. It happens in comparisons (WHERE hire_date > '03/04/2026'), in INSERTs into DATE columns, and in function arguments that expect dates. The code looks innocent — no conversion function in sight — which is exactly why it survives code review and dies in production.

The danger is context-dependence. The same statement means different things in different sessions, so testing proves nothing unless the test session matches production. Developers test in SQL Developer (one default), CI runs SQL*Plus (another), and the batch scheduler uses a service account with a third. All three can disagree while pointing at the same database, and each environment's tests pass with full sincerity.

Hunt implicit conversions like you'd hunt SQL injection: search the codebase for DATE columns compared against quoted literals, then wrap each in TO_DATE with the mask the data actually uses. The explicit mask documents the data contract in the code itself — the next engineer reads the statement and knows the format without checking any session setting.

explicit_conversion.sqlSQL
1
2
3
4
5
6
7
8
9
10
-- Loaded gun: the session's NLS_DATE_FORMAT decides the meaning
-- SELECT * FROM payroll WHERE pay_date > '03/04/2026';

-- Explicit: the statement carries its own format contract
SELECT * FROM payroll
WHERE pay_date > TO_DATE('03/04/2026', 'DD/MM/YYYY');

-- Safest for fixed dates: ANSI literal, immune to NLS entirely
SELECT * FROM payroll
WHERE pay_date > DATE '2026-04-03';
📊 Production Insight
A grep for quoted literals against DATE columns found 47 implicit conversions in one payroll module — each a future 1:40 AM page waiting for the right session.
🎯 Key Takeaway
Bare date literals inherit the session's mask — wrap every one in TO_DATE or replace it with a bind.

NLS Settings: Session vs Environment

NLS_DATE_FORMAT lives at three levels: the database default, the client environment (NLS_LANG), and the session (ALTER SESSION). The session wins, the client overrides the database, and tools set the client differently — SQL Developer derives it from its preferences, JDBC from connection properties or the JVM locale, and cron from whatever environment the service account has, which is often the C locale nobody chose deliberately.

This layering is why diffing databases proves nothing. Two sessions on the same database, same user, same query can parse dates differently because their clients set different NLS. The incident above had identical database parameters everywhere — the difference lived in the scheduler's environment, a file nobody had opened since the server was provisioned three years earlier.

Read the effective settings from inside the failing session: NLS_SESSION_PARAMETERS shows what that session actually uses, not what the database defaults to. Diff dev against prod at the session level, then pin the batch session explicitly with ALTER SESSION or connection properties. Pinned sessions make the behavior a property of the code, not of whichever machine runs it.

diff_nls.sqlSQL
1
2
3
4
5
6
7
8
9
10
-- Effective settings for THIS session: the only ones that matter
SELECT parameter, value FROM NLS_SESSION_PARAMETERS
WHERE parameter IN ('NLS_DATE_FORMAT', 'NLS_DATE_LANGUAGE', 'NLS_TERRITORY');

-- Database defaults for comparison (these lose to session/client)
SELECT parameter, value FROM NLS_DATABASE_PARAMETERS
WHERE parameter IN ('NLS_DATE_FORMAT', 'NLS_DATE_LANGUAGE', 'NLS_TERRITORY');

-- Pin the batch session explicitly before converting anything
ALTER SESSION SET NLS_DATE_FORMAT = 'DD/MM/YYYY';
💡Explicit Masks Help the Optimizer Too
Implicit conversion also kills index usage in some plans and corrupts partition pruning. The explicit mask isn't just correctness — the optimizer thanks you with stable plans.
📊 Production Insight
The scheduler's NLS_LANG lived in a profile script from 2021 — three years of correct runs ended when the feed's first day-13 date arrived that April.
🎯 Key Takeaway
Diff NLS at the session level, not the database level — then pin the batch session so behavior travels with the code.

DD/MM vs MM/DD, and the RR Century Trap

DD/MM versus MM/DD is the ambiguity that errors can't catch. Any date with day 12 or below parses under both masks into different months, so the wrong reading flows silently into storage. Day 13 and above crashes loudly with 01843 — which means the crash is actually the lucky outcome, because it points at the mask while the silent rows keep lying.

Years have their own trap in the RR format. RR maps two-digit years into a sliding window: years 00-49 land in the current century, 50-99 in the previous one. So '49 becomes 2049 while '50 becomes 1950, and the boundary slides every year — code that's correct today misfiles dates next January. RRRR (four-digit) has no window and no surprise, which is why storage should always carry four-digit years.

The portable defense is validating parts before converting. Splitting the string and range-checking day (1-31) and month (1-12) catches poison rows in any database, and the same logic ports into the staging gate below. Masks declare intent; part-checks prove the data deserves the mask.

validate_parts.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
-- Portable part-check: flags poison rows in any database
CREATE TABLE pay_staging (id INTEGER PRIMARY KEY, raw_day TEXT);
INSERT INTO pay_staging VALUES (1, '03/04/2026'), (2, '13/04/2026');

-- Split the parts: day first under DD/MM convention
SELECT raw_day, substr(raw_day, 1, 2) AS day_part,
  substr(raw_day, 4, 2) AS month_part FROM pay_staging;

-- Month 13 can never exist: quarantine these before converting
SELECT raw_day FROM pay_staging
WHERE CAST(substr(raw_day, 4, 2) AS INTEGER) > 12;
📊 Production Insight
Part-validation in staging caught 212 poison rows the next month-end — including six February 30ths that every mask would have rejected mid-batch.
🎯 Key Takeaway
Ambiguous dates swap silently while RR years slide — validate parts, demand four-digit years, standardize one mask.

Why Dev Passes and Prod Fails

Dev-pass/prod-fail happens because no two tools agree on defaults. SQL Developer ships its own NLS preferences, SQL*Plus inherits the shell environment, JDBC drivers consult connection properties and the JVM locale, and schedulers run as service accounts with skeletal environments. Each link in the chain from laptop to production can silently change the mask, and every link's tests pass because each link is self-consistent.

The environment check is unglamorous and decisive: print NLS_LANG in the exact context that runs the batch. Cron strips most environment variables, Docker images bake in whatever the base image had, and connection pools open sessions long before your code sets anything. The incident's scheduler ran with NLS_LANG unset, which fell back to MM/DD behavior — a default nobody selected and everybody inherited.

Remove the chain's power over you. Set NLS explicitly in the batch's session setup, pass dates as binds from application code, and keep one integration test that runs the conversion under production's NLS parameters. When the setting travels with the code, the machine it runs on stops mattering.

check_nls_env.shBASH
1
2
3
4
5
6
7
# Print NLS in the exact context that runs the batch
 echo "NLS_LANG=$NLS_LANG"
 locale | grep -i -E 'lang|lc_time'
 # For JDBC batches, dump the relevant connection properties
 grep -i -E 'nls|dateFormat|locale' app-datasource.properties
 # Cron context: compare interactive vs scheduled environments
 # * * * * * oracle env | grep -i nls > /tmp/cron-nls.txt
📊 Production Insight
The cron environment had no NLS_LANG at all — the fallback nobody chose decided every date parse for three years until day 13 arrived.
🎯 Key Takeaway
Every tool in the chain sets its own NLS — print it in the batch's context, then pin it in code.

Prevention: Literals, Binds, and Staging Gates

Prevention has three layers, cheapest first. Store DATE in DATE columns — never strings — so the database holds resolved values instead of format puzzles. Pass values as bind variables from application code so no conversion happens at all. And validate feeds in staging with part-checks before the batch touches them, quarantining poison rows instead of converting around them.

For the SQL you must write, prefer ANSI DATE literals for fixed dates: DATE '2026-04-03' parses identically in every session on every continent. Dynamic strings get TO_DATE with an explicit mask, and month names get numeric treatment — store 04, not APR, and let display formatting translate at the edge. Add a CHECK constraint or validation function on staging tables so malformed dates fail at entry with your message, not mid-batch with Oracle's.

Codify it in review: any bare quoted literal compared against a DATE column fails the build. The lint rule takes an afternoon to write and ends the entire error class — 01843 becomes a story seniors tell instead of a page juniors receive.

staging_gate.sqlSQL
1
2
3
4
5
6
7
8
9
10
-- Staging gate: only validated rows reach the batch
ALTER TABLE pay_staging ADD validated CHAR(1) DEFAULT 'N';

UPDATE pay_staging SET validated = 'Y'
WHERE raw_day GLOB '[0-9][0-9]/[0-9][0-9]/[0-9][0-9][0-9][0-9]'
  AND CAST(substr(raw_day, 4, 2) AS INTEGER) BETWEEN 1 AND 12
  AND CAST(substr(raw_day, 1, 2) AS INTEGER) BETWEEN 1 AND 31;

-- Batch reads validated rows only; the rest wait in quarantine
SELECT raw_day FROM pay_staging WHERE validated = 'N';
⚠ Schedulers Inherit Nothing
Cron and systemd strip environment variables your interactive shell takes for granted. Always verify NLS from inside the scheduled context, never from your laptop.
📊 Production Insight
The lint rule for bare date literals flagged 47 sites in week one — month-end has been silent for the eight cycles since.
🎯 Key Takeaway
Store DATEs, bind values, validate feeds in staging, and lint bare literals out of the codebase.
● Production incidentPOST-MORTEMseverity: high

A Payroll Batch Read April as March, Then Met the 13th Month

Symptom
At 1:40 AM the April payroll batch aborted with ORA-01843 after processing 400,000 of 1.1 million rows. The 4,300 payslips behind those rows missed the 6 AM direct-deposit window, and the rerun failed on the same row. Sampling showed values like '13/04/2026' — valid under the feed's DD/MM/YYYY convention, impossible as MM/DD. Earlier rows with days 1-12 had converted without errors into wrong months.
Assumption
The team blamed dirty data and spent two hours sampling the feed, which looked clean — every value resembled a date. A second theory blamed the April month-end volume, since the job had never processed this many rows. Both theories were wrong: the feed was fine and volume was irrelevant, because 01843 cares about format, not quantity.
Root cause
The batch compared DATE columns against bare string literals, so Oracle applied each session's NLS_DATE_FORMAT. Developers' tools defaulted to DD/MM/YYYY while the scheduler's service account used MM/DD/YYYY. Every date with day above 12 crashed with 01843; worse, dates with day 12 or below parsed into the wrong month silently for months before anyone noticed the swapped payslip dates.
Fix
They wrapped every date literal in TO_DATE with the DD/MM/YYYY mask the feed actually uses, pinned NLS_DATE_FORMAT in the batch session setup, and reran from the checkpoint at 4:55 AM. Payslips completed by 6:10 AM. The next week the loader switched to bind variables with DATE columns, and a staging validation query now quarantines malformed dates before month-end.
Key lesson
  • Reproduce on one row before theorizing about millions: a single TO_DATE probe would have named the mask in minutes instead of two hours of feed sampling.
  • Session settings are deployment configuration — diff NLS parameters across environments with the same rigor as schema diffs, or dev-pass/prod-fail will recur.
  • Checkpointed batches turn a 4-hour failure into a rerun from row 400,000: resume capability matters more than hoping the run never fails.
Production debug guideFive steps from cryptic rejection to explicit, provable conversion.5 entries
Symptom · 01
The batch fails and you need to reproduce it on one row
→
Fix
Wrap the literal both ways and see which parses: SELECT TO_DATE('03/04/2026','DD/MM/YYYY') FROM DUAL; versus SELECT TO_DATE('03/04/2026','MM/DD/YYYY') FROM DUAL; Then try the failing value: SELECT TO_DATE('13/04/2026','MM/DD/YYYY') FROM DUAL; reproduces 01843 on demand, which proves the mask — not the data volume — is the suspect.
Symptom · 02
It works in dev but dies in production
→
Fix
Run SELECT parameter, value FROM NLS_SESSION_PARAMETERS WHERE parameter LIKE 'NLS_DATE%'; in both environments and diff NLS_DATE_FORMAT, NLS_DATE_LANGUAGE, and NLS_TERRITORY. Any difference in the first two explains dev-pass/prod-fail without touching the code.
Symptom · 03
The database settings match but sessions still differ
→
Fix
Check the client's NLS_LANG: echo $NLS_LANG on Unix, or echo %NLS_LANG% on Windows, plus the JDBC connection properties for oracle.jdbc.dateFormat. Connection pools and schedulers often run under a service account whose environment nobody reviewed since setup day.
Symptom · 04
A feed with millions of rows hides a few poison values
→
Fix
Hunt bad rows before converting: SELECT raw FROM staging WHERE NOT REGEXP_LIKE(raw, '^[0-9]{2}/[0-9]{2}/[0-9]{4}$'); then check month parts: SELECT raw FROM staging WHERE TO_NUMBER(SUBSTR(raw, 4, 2)) > 12; Quarantine the hits instead of converting around them.
Symptom · 05
You know the mask — now load safely
→
Fix
Convert explicitly and defensively: INSERT INTO t (d) SELECT TO_DATE(raw, 'DD/MM/YYYY') FROM staging WHERE validated = 'Y'; Prefer DATE '2026-04-03' literals for fixed dates and bind variables from the app. Rerun the full batch in staging under production's NLS before the real run.
ORA-01843 Causes at a Glance
Root CauseHow to ConfirmFixPrevention
Implicit string-to-date conversionLiteral compared against a DATE column with no TO_DATEWrap every literal in TO_DATE with an explicit maskLint for bare date literals in code review
Session NLS differs from devNLS_SESSION_PARAMETERS differs between environmentsSet NLS explicitly per session or use literalsPin NLS in connection setup, alert on drift
DD/MM versus MM/DD mismatchSame string parses differently under two masksStandardize one mask org-wide; prefer ISOStore DATE, format only at display time
Month name in wrong languageNLS_DATE_LANGUAGE doesn't match the literalTO_DATE with the language argument or numeric monthsNever store month names; use numbers or DATE
Bad rows in the feedValidation query flags month parts over 12Quarantine bad rows, fix the source feedCHECK constraints plus staging validation
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
reproduce_01843.sqlSELECT TO_DATE('03/04/2026', 'DD/MM/YYYY') AS dd_mm FROM DUAL;What ORA-01843 Actually Rejects
explicit_conversion.sqlSELECT * FROM payrollImplicit Conversion Is the Loaded Gun
diff_nls.sqlSELECT parameter, value FROM NLS_SESSION_PARAMETERSNLS Settings
validate_parts.sqlCREATE TABLE pay_staging (id INTEGER PRIMARY KEY, raw_day TEXT);DD/MM vs MM/DD, and the RR Century Trap
check_nls_env.shecho "NLS_LANG=$NLS_LANG"Why Dev Passes and Prod Fails
staging_gate.sqlALTER TABLE pay_staging ADD validated CHAR(1) DEFAULT 'N';Prevention

Key takeaways

1
ORA-01843 is a failed month token
wrong number, wrong language, or wrong assumed format.
2
Implicit conversion lets the session's NLS_DATE_FORMAT decide what your string means
never allow it.
3
Compare NLS_SESSION_PARAMETERS across environments before blaming the data.
4
DD/MM versus MM/DD swaps months silently, so standardize one mask and store DATE columns.
5
TO_DATE with an explicit mask fixes today's statement; binds and DATE literals prevent repeats.
6
Validate feeds in staging
garbage month parts belong in quarantine, not in clever masks.

Common mistakes to avoid

5 patterns
×

Wrapping the failing literal in TO_DATE with a guessed mask

Symptom
The error moves to a different row instead of disappearing — the mask was wrong, not missing.
Fix
Wrap the literal in TO_DATE with the mask the data actually uses, then standardize that mask everywhere.
×

Testing only on a laptop whose NLS matches the data

Symptom
Every test passes locally while production rejects the same statements with 01843.
Fix
Compare NLS_SESSION_PARAMETERS in both environments and pin them in session setup code.
×

Mixing DD/MM and MM/DD literals across modules

Symptom
March and April swap silently on some rows while others crash — wrong dates plus errors together.
Fix
Pick one convention (ISO YYYY-MM-DD), convert at the boundary, and store DATE columns only.
×

Storing month names like 'JAN' in staging tables

Symptom
A session with a different NLS_DATE_LANGUAGE rejects every name it can't pronounce.
Fix
Pass month numbers or DATE binds; keep names for display formatting with an explicit language.
×

Using RR years without knowing what century they land in

Symptom
'49 becomes 2049 while '50 becomes 1950 — birth dates and expiry dates land a century off.
Fix
Keep the two-digit year behavior explicit with RR or, better, four-digit years everywhere.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does ORA-01843 mean?
Q02SENIOR
What is implicit string-to-date conversion, and why is it dangerous?
Q03SENIOR
Same query passes in dev and fails in prod. How do you investigate?
Q04SENIOR
Why is DD/MM versus MM/DD worse than a crash?
Q05SENIOR
Explain the RR versus RRRR century trap.
Q01 of 05JUNIOR

What does ORA-01843 mean?

ANSWER
Oracle can't resolve the month token in a string-to-date conversion — a number over 12, a month name in the wrong session language, or a string parsed under the wrong format mask.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What exactly does ORA-01843 complain about?
02
Is the default date format the same everywhere?
03
TO_DATE or bind variables — which fix should I pick?
04
Are ANSI DATE literals safe under any NLS?
05
How do I find the bad rows in a 2-million-row feed?
06
Why does 'JAN' work but 'JANV' fail?
N
Naren Founder & Principal Engineer

20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.

Follow
✓ Verified
production tested
October 27, 2026
last updated
2,087
articles · all by Naren
🔥

That's Oracle. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
ORA-01400 Cannot Insert NULL Fix
4 / 6 · Oracle
Next
ORA-02291 Integrity Constraint Fix
→