@@ -599,6 +599,241 @@ describe("ServerError", () => {
599599 } ) ;
600600} ) ;
601601
602+ // =========================================================== //
603+ // Recursive cause chain //
604+ // =========================================================== //
605+
606+ describe ( "cause chain" , ( ) => {
607+ test ( "RPC error with single cause" , ( ) => {
608+ const err = parseRpcError ( {
609+ code : - 32000 ,
610+ kind : "Internal" ,
611+ message : "Outer error" ,
612+ cause : {
613+ kind : "NotFound" ,
614+ message : "Inner: table missing" ,
615+ details : { kind : "Table" , details : { name : "users" } } ,
616+ } ,
617+ } ) ;
618+
619+ expect ( err ) . toBeInstanceOf ( InternalError ) ;
620+ expect ( err . message ) . toBe ( "Outer error" ) ;
621+
622+ expect ( err . cause ) . toBeDefined ( ) ;
623+ const inner = err . cause as ServerError ;
624+ expect ( inner ) . toBeInstanceOf ( NotFoundError ) ;
625+ expect ( inner ) . toBeInstanceOf ( ServerError ) ;
626+ expect ( inner . kind ) . toBe ( "NotFound" ) ;
627+ expect ( inner . message ) . toBe ( "Inner: table missing" ) ;
628+ expect ( ( inner as NotFoundError ) . tableName ) . toBe ( "users" ) ;
629+ expect ( inner . cause ) . toBeUndefined ( ) ;
630+ } ) ;
631+
632+ test ( "RPC error with deeply nested cause chain" , ( ) => {
633+ const err = parseRpcError ( {
634+ code : - 32002 ,
635+ kind : "NotAllowed" ,
636+ message : "Permission denied" ,
637+ cause : {
638+ kind : "Validation" ,
639+ message : "Bad input" ,
640+ cause : {
641+ kind : "Internal" ,
642+ message : "Root cause" ,
643+ } ,
644+ } ,
645+ } ) ;
646+
647+ expect ( err ) . toBeInstanceOf ( NotAllowedError ) ;
648+ expect ( err . message ) . toBe ( "Permission denied" ) ;
649+
650+ expect ( err . cause ) . toBeDefined ( ) ;
651+ const mid = err . cause as ServerError ;
652+ expect ( mid ) . toBeInstanceOf ( ValidationError ) ;
653+ expect ( mid . message ) . toBe ( "Bad input" ) ;
654+
655+ expect ( mid . cause ) . toBeDefined ( ) ;
656+ const root = mid . cause as ServerError ;
657+ expect ( root ) . toBeInstanceOf ( InternalError ) ;
658+ expect ( root . message ) . toBe ( "Root cause" ) ;
659+ expect ( root . cause ) . toBeUndefined ( ) ;
660+ } ) ;
661+
662+ test ( "query error with cause" , ( ) => {
663+ const err = parseQueryError ( {
664+ status : "ERR" ,
665+ time : "1ms" ,
666+ result : "Query failed" ,
667+ kind : "Query" ,
668+ details : { kind : "Cancelled" } ,
669+ cause : {
670+ kind : "Internal" ,
671+ message : "Backend unavailable" ,
672+ } ,
673+ } ) ;
674+
675+ expect ( err ) . toBeInstanceOf ( QueryError ) ;
676+ expect ( ( err as QueryError ) . isCancelled ) . toBe ( true ) ;
677+
678+ expect ( err . cause ) . toBeDefined ( ) ;
679+ const inner = err . cause as ServerError ;
680+ expect ( inner ) . toBeInstanceOf ( InternalError ) ;
681+ expect ( inner . message ) . toBe ( "Backend unavailable" ) ;
682+ } ) ;
683+
684+ test ( "cause with null or missing cause terminates chain" , ( ) => {
685+ const err = parseRpcError ( {
686+ code : - 32000 ,
687+ kind : "Internal" ,
688+ message : "Outer" ,
689+ cause : {
690+ kind : "Internal" ,
691+ message : "Inner" ,
692+ cause : null ,
693+ } ,
694+ } ) ;
695+
696+ expect ( err . cause ) . toBeDefined ( ) ;
697+ expect ( ( err . cause as ServerError ) . cause ) . toBeUndefined ( ) ;
698+ } ) ;
699+
700+ test ( "null cause on top-level produces no cause" , ( ) => {
701+ const err = parseRpcError ( {
702+ code : - 32000 ,
703+ kind : "Internal" ,
704+ message : "No cause" ,
705+ cause : null ,
706+ } ) ;
707+
708+ expect ( err . cause ) . toBeUndefined ( ) ;
709+ } ) ;
710+
711+ test ( "missing cause field produces no cause" , ( ) => {
712+ const err = parseRpcError ( {
713+ code : - 32000 ,
714+ kind : "Internal" ,
715+ message : "No cause" ,
716+ } ) ;
717+
718+ expect ( err . cause ) . toBeUndefined ( ) ;
719+ } ) ;
720+
721+ test ( "cause inherits correct subclass based on kind" , ( ) => {
722+ const err = parseRpcError ( {
723+ code : - 32000 ,
724+ kind : "Internal" ,
725+ message : "Wrapper" ,
726+ cause : {
727+ kind : "AlreadyExists" ,
728+ message : "Duplicate record" ,
729+ details : { kind : "Record" , details : { id : "person:1" } } ,
730+ } ,
731+ } ) ;
732+
733+ expect ( err . cause ) . toBeDefined ( ) ;
734+ const inner = err . cause as AlreadyExistsError ;
735+ expect ( inner ) . toBeInstanceOf ( AlreadyExistsError ) ;
736+ expect ( inner . recordId ) . toBe ( "person:1" ) ;
737+ } ) ;
738+
739+ test ( "cause with unknown kind creates base ServerError" , ( ) => {
740+ const err = parseRpcError ( {
741+ code : - 32000 ,
742+ kind : "Internal" ,
743+ message : "Wrapper" ,
744+ cause : {
745+ kind : "FutureKind" ,
746+ message : "From a newer server" ,
747+ details : { kind : "NewDetail" } ,
748+ } ,
749+ } ) ;
750+
751+ expect ( err . cause ) . toBeDefined ( ) ;
752+ const inner = err . cause as ServerError ;
753+ expect ( inner ) . toBeInstanceOf ( ServerError ) ;
754+ expect ( inner ) . not . toBeInstanceOf ( InternalError ) ;
755+ expect ( inner . kind ) . toBe ( "FutureKind" ) ;
756+ expect ( inner . details ) . toEqual ( { kind : "NewDetail" } ) ;
757+ } ) ;
758+
759+ test ( "cause is the native Error.cause (JS error chaining)" , ( ) => {
760+ const err = parseRpcError ( {
761+ code : - 32000 ,
762+ kind : "Internal" ,
763+ message : "Outer" ,
764+ cause : {
765+ kind : "NotFound" ,
766+ message : "Inner" ,
767+ } ,
768+ } ) ;
769+
770+ const nativeCause = ( err as Error ) . cause ;
771+ expect ( nativeCause ) . toBe ( err . cause ) ;
772+ expect ( nativeCause ) . toBeInstanceOf ( NotFoundError ) ;
773+ } ) ;
774+
775+ test ( "ServerError constructed directly with cause" , ( ) => {
776+ const inner = new NotFoundError ( {
777+ kind : "NotFound" ,
778+ message : "table missing" ,
779+ } ) ;
780+
781+ const outer = new ServerError ( {
782+ kind : "Internal" ,
783+ message : "wrapped" ,
784+ cause : inner ,
785+ } ) ;
786+
787+ expect ( outer . cause ) . toBe ( inner ) ;
788+ expect ( ( outer as Error ) . cause ) . toBe ( inner ) ;
789+ expect ( outer . cause ) . toBeInstanceOf ( NotFoundError ) ;
790+ } ) ;
791+
792+ test ( "ServerError constructed without cause has undefined cause" , ( ) => {
793+ const err = new ServerError ( {
794+ kind : "Internal" ,
795+ message : "no cause" ,
796+ } ) ;
797+
798+ expect ( err . cause ) . toBeUndefined ( ) ;
799+ } ) ;
800+
801+ test ( "thrown error preserves cause chain when caught" , ( ) => {
802+ const err = parseRpcError ( {
803+ code : - 32002 ,
804+ kind : "NotAllowed" ,
805+ message : "Permission denied" ,
806+ cause : {
807+ kind : "Validation" ,
808+ message : "Invalid token format" ,
809+ cause : {
810+ kind : "Internal" ,
811+ message : "Root cause: decode failed" ,
812+ } ,
813+ } ,
814+ } ) ;
815+
816+ try {
817+ throw err ;
818+ } catch ( caught ) {
819+ expect ( caught ) . toBeInstanceOf ( NotAllowedError ) ;
820+ const e = caught as ServerError ;
821+ expect ( e . message ) . toBe ( "Permission denied" ) ;
822+
823+ expect ( e . cause ) . toBeDefined ( ) ;
824+ const mid = e . cause as ServerError ;
825+ expect ( mid ) . toBeInstanceOf ( ValidationError ) ;
826+ expect ( mid . message ) . toBe ( "Invalid token format" ) ;
827+
828+ expect ( mid . cause ) . toBeDefined ( ) ;
829+ const root = mid . cause as ServerError ;
830+ expect ( root ) . toBeInstanceOf ( InternalError ) ;
831+ expect ( root . message ) . toBe ( "Root cause: decode failed" ) ;
832+ expect ( root . cause ) . toBeUndefined ( ) ;
833+ }
834+ } ) ;
835+ } ) ;
836+
602837// =========================================================== //
603838// ResponseError backward compat alias //
604839// =========================================================== //
0 commit comments