@@ -187,7 +187,7 @@ describe(`Transactions`, () => {
187187 } )
188188 } )
189189
190- transaction . commit ( )
190+ await expect ( transaction . commit ( ) ) . rejects . toThrow ( `bad` )
191191
192192 await expect ( transaction . isPersisted . promise ) . rejects . toThrow ( `bad` )
193193 transaction . isPersisted . promise . catch ( ( ) => { } )
@@ -223,14 +223,276 @@ describe(`Transactions`, () => {
223223 } )
224224 } )
225225
226- transaction . commit ( )
226+ await expect ( transaction . commit ( ) ) . rejects . toThrow ( `bad` )
227227
228228 await expect ( transaction . isPersisted . promise ) . rejects . toThrow ( `bad` )
229229 transaction . isPersisted . promise . catch ( ( ) => { } )
230230 expect ( transaction . state ) . toBe ( `failed` )
231231 expect ( transaction . error ?. message ) . toBe ( `bad` )
232232 expect ( transaction . error ?. error ) . toBeInstanceOf ( Error )
233233 } )
234+ it ( `commit() should throw errors when mutation function fails` , async ( ) => {
235+ const tx = createTransaction ( {
236+ mutationFn : async ( ) => {
237+ throw new Error ( `API failed` )
238+ } ,
239+ autoCommit : false ,
240+ } )
241+
242+ const collection = createCollection < {
243+ id : string
244+ text : string
245+ } > ( {
246+ id : `test-collection` ,
247+ getKey : ( item ) => item . id ,
248+ sync : {
249+ sync : ( ) => { } ,
250+ } ,
251+ } )
252+
253+ tx . mutate ( ( ) => {
254+ collection . insert ( { id : `1` , text : `Item` } )
255+ } )
256+
257+ try {
258+ await tx . commit ( )
259+ expect . fail ( `Expected commit to throw` )
260+ } catch ( error ) {
261+ // Transaction has been rolled back
262+ expect ( tx . state ) . toBe ( `failed` )
263+ expect ( tx . error ?. message ) . toBe ( `API failed` )
264+ expect ( error ) . toBeInstanceOf ( Error )
265+ expect ( ( error as Error ) . message ) . toBe ( `API failed` )
266+ }
267+ } )
268+
269+ it ( `commit() and isPersisted.promise should reject with the same error instance` , async ( ) => {
270+ const originalError = new Error ( `Original API error` )
271+ const tx = createTransaction ( {
272+ mutationFn : async ( ) => {
273+ throw originalError
274+ } ,
275+ autoCommit : false ,
276+ } )
277+
278+ const collection = createCollection < {
279+ id : string
280+ text : string
281+ } > ( {
282+ id : `test-collection` ,
283+ getKey : ( item ) => item . id ,
284+ sync : {
285+ sync : ( ) => { } ,
286+ } ,
287+ } )
288+
289+ tx . mutate ( ( ) => {
290+ collection . insert ( { id : `1` , text : `Item` } )
291+ } )
292+
293+ let commitError : unknown
294+ let persistedError : unknown
295+
296+ // Capture error from commit()
297+ try {
298+ await tx . commit ( )
299+ } catch ( error ) {
300+ commitError = error
301+ }
302+
303+ // Capture error from isPersisted.promise
304+ try {
305+ await tx . isPersisted . promise
306+ } catch ( error ) {
307+ persistedError = error
308+ }
309+
310+ // Both should be the exact same error instance
311+ expect ( commitError ) . toBe ( originalError )
312+ expect ( persistedError ) . toBe ( originalError )
313+ expect ( commitError ) . toBe ( persistedError )
314+ } )
315+
316+ it ( `should handle non-Error throwables (strings)` , async ( ) => {
317+ const tx = createTransaction ( {
318+ mutationFn : async ( ) => {
319+ throw `string error`
320+ } ,
321+ autoCommit : false ,
322+ } )
323+
324+ const collection = createCollection < {
325+ id : string
326+ } > ( {
327+ id : `test-collection` ,
328+ getKey : ( item ) => item . id ,
329+ sync : {
330+ sync : ( ) => { } ,
331+ } ,
332+ } )
333+
334+ tx . mutate ( ( ) => {
335+ collection . insert ( { id : `1` } )
336+ } )
337+
338+ try {
339+ await tx . commit ( )
340+ expect . fail ( `Expected commit to throw` )
341+ } catch ( error ) {
342+ // Should be converted to an Error object
343+ expect ( error ) . toBeInstanceOf ( Error )
344+ expect ( ( error as Error ) . message ) . toBe ( `string error` )
345+ }
346+
347+ // Same error from isPersisted.promise
348+ try {
349+ await tx . isPersisted . promise
350+ } catch ( error ) {
351+ expect ( error ) . toBeInstanceOf ( Error )
352+ expect ( ( error as Error ) . message ) . toBe ( `string error` )
353+ }
354+ } )
355+
356+ it ( `should handle non-Error throwables (numbers, objects)` , async ( ) => {
357+ const tx = createTransaction ( {
358+ mutationFn : async ( ) => {
359+ throw 42
360+ } ,
361+ autoCommit : false ,
362+ } )
363+
364+ tx . mutate ( ( ) => { } )
365+
366+ try {
367+ await tx . commit ( )
368+ } catch ( error ) {
369+ expect ( error ) . toBeInstanceOf ( Error )
370+ expect ( ( error as Error ) . message ) . toBe ( `42` )
371+ }
372+
373+ const tx2 = createTransaction ( {
374+ mutationFn : async ( ) => {
375+ throw { code : `ERR_FAILED` , details : `Something went wrong` }
376+ } ,
377+ autoCommit : false ,
378+ } )
379+
380+ tx2 . mutate ( ( ) => { } )
381+
382+ try {
383+ await tx2 . commit ( )
384+ } catch ( error ) {
385+ expect ( error ) . toBeInstanceOf ( Error )
386+ expect ( ( error as Error ) . message ) . toContain ( `[object Object]` )
387+ }
388+ } )
389+
390+ it ( `should throw TransactionNotPendingCommitError when commit() is called on completed transaction` , async ( ) => {
391+ const tx = createTransaction ( {
392+ mutationFn : async ( ) => Promise . resolve ( ) ,
393+ autoCommit : false ,
394+ } )
395+
396+ tx . mutate ( ( ) => { } )
397+
398+ // First commit succeeds
399+ await tx . commit ( )
400+ expect ( tx . state ) . toBe ( `completed` )
401+
402+ // Second commit should throw TransactionNotPendingCommitError
403+ await expect ( tx . commit ( ) ) . rejects . toThrow ( TransactionNotPendingCommitError )
404+ } )
405+
406+ it ( `should throw TransactionNotPendingCommitError when commit() is called on failed transaction` , async ( ) => {
407+ const tx = createTransaction ( {
408+ mutationFn : async ( ) => {
409+ throw new Error ( `Failed` )
410+ } ,
411+ autoCommit : false ,
412+ } )
413+
414+ const collection = createCollection < {
415+ id : string
416+ } > ( {
417+ id : `test-collection` ,
418+ getKey : ( item ) => item . id ,
419+ sync : {
420+ sync : ( ) => { } ,
421+ } ,
422+ } )
423+
424+ tx . mutate ( ( ) => {
425+ collection . insert ( { id : `1` } )
426+ } )
427+
428+ // First commit fails
429+ try {
430+ await tx . commit ( )
431+ } catch {
432+ // Expected to fail
433+ }
434+ expect ( tx . state ) . toBe ( `failed` )
435+
436+ // Second commit should throw TransactionNotPendingCommitError
437+ await expect ( tx . commit ( ) ) . rejects . toThrow ( TransactionNotPendingCommitError )
438+ } )
439+
440+ it ( `should handle cascading rollbacks with proper error propagation` , async ( ) => {
441+ const originalError = new Error ( `Primary transaction failed` )
442+ const tx1 = createTransaction ( {
443+ mutationFn : async ( ) => {
444+ throw originalError
445+ } ,
446+ autoCommit : false ,
447+ } )
448+ const tx2 = createTransaction ( {
449+ mutationFn : async ( ) => Promise . resolve ( ) ,
450+ autoCommit : false ,
451+ } )
452+
453+ const collection = createCollection < {
454+ id : string
455+ value : string
456+ } > ( {
457+ id : `test-collection` ,
458+ getKey : ( item ) => item . id ,
459+ sync : {
460+ sync : ( ) => { } ,
461+ } ,
462+ } )
463+
464+ // Both transactions insert items - tx2 will depend on tx1's item
465+ tx1 . mutate ( ( ) => {
466+ collection . insert ( { id : `item-1` , value : `from-tx1` } )
467+ } )
468+
469+ tx2 . mutate ( ( ) => {
470+ // Insert an item that references tx1's item, creating a dependency
471+ collection . insert ( { id : `item-1-copy` , value : `copied-from-tx1` } )
472+ collection . update ( `item-1` , ( draft ) => {
473+ draft . value = `modified-by-tx2`
474+ } )
475+ } )
476+
477+ // tx1 commit fails and should cascade rollback to tx2
478+ let tx1CommitError : unknown
479+ try {
480+ await tx1 . commit ( )
481+ } catch ( error ) {
482+ tx1CommitError = error
483+ }
484+
485+ // Verify both transactions are failed
486+ expect ( tx1 . state ) . toBe ( `failed` )
487+ expect ( tx2 . state ) . toBe ( `failed` )
488+
489+ // tx1 should throw the original error
490+ expect ( tx1CommitError ) . toBe ( originalError )
491+
492+ // tx2's isPersisted.promise should also be rejected (but with undefined since it's a cascading rollback)
493+ await expect ( tx2 . isPersisted . promise ) . rejects . toBeUndefined ( )
494+ } )
495+
234496 it ( `should, when rolling back, find any other pending transactions w/ overlapping mutations and roll them back as well` , async ( ) => {
235497 const transaction1 = createTransaction ( {
236498 mutationFn : async ( ) => Promise . resolve ( ) ,
0 commit comments