Community Knowledge > Gotchas and Pitfalls
Where Journal Recording Limitations and Undo-Mark Pitfalls Collide
This article connects two gotchas documented separately elsewhere in this knowledge base: the recorder limitations in journal-recording-limitations.md and the undo-mark handling rules in session-undo-marks-pitfalls.md. Neither article alone predicts the failure mode below; it only shows up once you productionize a recorded journal that also sets its own undo mark.
The setup: a recorded journal, lightly hardened
A typical path into this problem: someone records a journal for an operation, then wraps it in the standard undo-mark pattern from session-undo-marks-pitfalls.md so it can be re-run safely:
Session.UndoMarkId markId = theSession.SetUndoMark(
Session.MarkVisibility.Visible, "Recorded Operation");
try
{
// recorded builder code, largely unmodified
var builder = workPart.Features.CreateSomeBuilder();
builder.SomeLiteralValue = 12.5; // recorded literal, per journal-recording-limitations.md
NXObject result = builder.Commit();
builder.Destroy();
theSession.UpdateManager.DoUpdate(markId);
}
catch (Exception)
{
theSession.UndoToMark(markId, "Recorded Operation");
throw;
}
This looks correct against the pattern in session-undo-marks-pitfalls.md. The problem is what the recorder limitations do to the try block's failure modes.
Why recorded journals fail partway through Commit() more than hand-written code does
As documented in journal-recording-limitations.md, a recorded journal can omit a builder property that happened to sit at its default value during the recording session, even though that property is required for Commit() to succeed under other input conditions. Combined with the fact that recorded values are literal, not parameterized, a recorded journal turned into a reusable tool is far more likely than hand-written code to hit exactly the case the recorder never saw, and therefore to throw partway through Commit().
When Commit() throws:
- The undo mark set by
SetUndoMarkbefore thetryblock still exists. - Depending on how far
Commit()got before failing, NX may have already applied partial model state under that mark. - The
catchblock'stheSession.UndoToMark(markId, ...)call is what is supposed to clean this up, by rolling the model back to the mark.
So far this matches the documented pattern. The interaction problem shows up in the cases people actually hit once they start reusing recorded code: multi-step recorded journals.
The dangling-mark case: multi-step recorded journals
Because journal code lives in a single file with no external libraries (journal-recording-limitations.md), a recorded journal that performs several related operations typically records them as a sequence of separate builder blocks in that one file, each with its own Commit(), rather than as calls into shared, separately-tested helper functions. A common way to "productionize" that recorded sequence is to wrap the whole sequence in one outer undo mark, on the assumption that one mark covering everything is simpler than one per step:
Session.UndoMarkId markId = theSession.SetUndoMark(
Session.MarkVisibility.Visible, "Multi-Step Recorded Operation");
try
{
builder1.Commit(); // recorded step 1
builder2.Commit(); // recorded step 2, fails here on a real input
builder3.Commit(); // recorded step 3, never reached
theSession.UpdateManager.DoUpdate(markId);
}
catch (Exception)
{
theSession.UndoToMark(markId, "Multi-Step Recorded Operation");
throw;
}
If builder2.Commit() throws (for the reason above: a recorder-omitted property that this input actually needed), the catch block runs UndoToMark against the single outer mark. That is the correct call per session-undo-marks-pitfalls.md and it does roll back step 1's partial state. But two things commonly go wrong around it in practice:
- The rollback itself is skipped or fails. If the outer
try/catchisn't actually wrapping every recorded step (easy to get wrong when adapting single-file recorded code that wasn't structured with error handling in mind), or ifUndoToMarkis called with the wrong mark identifier because the recorded code's flow was restructured during productionization, the mark fromSetUndoMarkis left in the session with no corresponding undo call ever reaching it. This is the "dangling undo mark": a mark that was created, never rolled back to, and never explicitly cleaned up. - The wrong cleanup call is used to fix it. Per
session-undo-marks-pitfalls.md, conflating "delete this one mark" with "roll everything back to this mark" is a documented, repeated source of confusing behavior. Someone who notices a leftover undo mark after a failed recorded run and reaches forUndoToMarkagain (instead ofDeleteUndoMark) risks undoing marks created after the failure, including any cleanup or logging the surrounding tool did once it caught the exception.
The cleanup pattern this interaction actually needs
The fix is to treat a recorded journal's Commit() failure as an expected outcome, not an edge case, precisely because journal-recording-limitations.md establishes that recorded builder property sets are not guaranteed complete against arbitrary input. That means:
- Set the undo mark immediately before the recorded step(s) that might fail, not once at the top of a long multi-step sequence, so a partial-sequence failure has a narrow, well-understood mark to act on.
- On a caught
Commit()failure, decide explicitly whether the intent is "roll the model back" (UndoToMark) or "just discard the bookkeeping mark I created, without touching anything else" (DeleteUndoMark) — do not default to whichever call happens to be nearby in the recorded code, since recorded code doesn't contain this decision at all. - If the mark was already rolled back via
UndoToMarkinside thecatchblock, do not also callDeleteUndoMarkon it afterward on the assumption that "cleanup" always means both;UndoToMarkalready consumes the mark for that purpose. - If a tool sets a mark purely for its own bookkeeping around a recorded step (for example, to isolate that step's undo history from surrounding steps) and the step succeeds, clean up with
DeleteUndoMarkrather than leaving the mark in the session indefinitely, since an accumulating pile of unnamed marks from repeated tool runs is itself whatsession-undo-marks-pitfalls.mdcalls out as the multiplied cost.
Session.UndoMarkId markId = theSession.SetUndoMark(
Session.MarkVisibility.Invisible, "Recorded Step Boundary");
bool committed = false;
try
{
NXObject result = builder.Commit(); // recorder-omitted property may make this throw
committed = true;
}
finally
{
if (committed)
{
// Step succeeded: this mark was only internal bookkeeping, discard it
// without disturbing any marks the caller set up around this call.
theSession.DeleteUndoMark(markId, "Recorded Step Boundary");
}
else
{
// Step failed partway through Commit(): roll the model back to
// before this step, since Commit() may have applied partial state.
theSession.UndoToMark(markId, "Recorded Step Boundary");
}
}
Practical takeaway
- Recorded journals are more likely than hand-written code to throw partway through
Commit(), because the recorder can omit builder properties that only matter for inputs the recording session never exercised (journal-recording-limitations.md). - Every
Commit()call taken from a recorded journal and reused in a tool needs an undo mark scoped tightly enough that a partial failure has one clear, known mark to act on, not one mark shared across an entire multi-step recorded sequence. - On failure, roll the model back with
UndoToMark. On success, if the mark was only internal bookkeeping, discard it withDeleteUndoMarkinstead of leaving it dangling in the session or reaching forUndoToMarkand accidentally undoing marks set afterward.
Source: Synthesis of journal-recording-limitations.md and session-undo-marks-pitfalls.md in this knowledge base · retrieved 2026-09-25