diff --git a/crates/organize/src/outline.rs b/crates/organize/src/outline.rs
--- a/crates/organize/src/outline.rs
+++ b/crates/organize/src/outline.rs
@@ -691,6 +691,8 @@ pub fn move_bookmark(doc: &mut Document, from: &[usize], to_parent: &[usize], in
         return Err(OutlineError::IntoItself);
     }
     let (r, old_parent) = resolve_path(doc, from)?;
+    // Validate the target before touching the old parent, so an error leaves the outline intact.
+    let parent = container(doc, to_parent)?;
     let mut old = children_of(doc, old_parent, &mut HashSet::from([old_parent]));
     old.retain(|k| *k != r);
     relink(doc, old_parent, &old)?;
@@ -700,7 +702,6 @@ pub fn move_bookmark(doc: &mut Document, from: &[usize], to_parent: &[usize], in
     if to_parent.len() > depth && to_parent[..depth] == from[..depth] && to_parent[depth] > from[depth] {
         to_parent[depth] -= 1;
     }
-    let parent = container(doc, &to_parent)?;
     let mut kids = children_of(doc, parent, &mut HashSet::from([parent]));
     let at = index.min(kids.len());
     kids.insert(at, r);
diff --git a/crates/organize/src/tests.rs b/crates/organize/src/tests.rs
--- a/crates/organize/src/tests.rs
+++ b/crates/organize/src/tests.rs
@@ -1731,6 +1731,15 @@ fn count_of(doc: &Document, r: ObjRef) -> Option<i64> {
     doc.get(r).as_dict().and_then(|d| d.int(b"Count"))
 }
 
+#[test]
+fn move_bookmark_to_missing_parent_keeps_outline() {
+    let mut d = doc_a();
+    crate::add_bookmark(&mut d, &[], 0, "Intro", 0).unwrap();
+    crate::add_bookmark(&mut d, &[], 9, "Results", 2).unwrap();
+    assert!(crate::move_bookmark(&mut d, &[0], &[7], 0).is_err());
+    assert_eq!(titles(&crate::bookmarks(&d)), ["Intro", "Results"]);
+}
+
 #[test]
 fn bookmarks_add_rename_move_delete() {
     let mut d = doc_a();
