314
'RangeError: Maximum call stack size exceeded',
315
);
316
});
317
+
318
+ it('client renders when an error is thrown in an error boundary', async () => {
319
+ function Throws() {
320
+ throw new Error('plain error');
321
+ }
322
+
323
+ class ErrorBoundary extends React.Component {
324
+ state = {error: null};
325
+ static getDerivedStateFromError(error) {
326
+ return {error};
327
+ }
328
+ render() {
329
+ if (this.state.error) {
330
+ return <div>Caught an error: {this.state.error.message}</div>;
331
+ }
332
+ return this.props.children;
333
+ }
334
+ }
335
+
336
+ function App() {
337
+ return (
338
+ <ErrorBoundary>
339
+ <Throws />
340
+ </ErrorBoundary>
341
+ );
342
+ }
343
+
344
+ // Server render
345
+ let shellError;
346
+ try {
347
+ await serverAct(async () => {
348
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
349
+ onError(error) {
350
+ Scheduler.log('onError: ' + error.message);
351
+ },
352
+ });
353
+ pipe(writable);
354
+ });
355
+ } catch (x) {
356
+ shellError = x;
357
+ }
358
+ expect(shellError).toEqual(
359
+ expect.objectContaining({message: 'plain error'}),
360
+ );
361
+ assertLog(['onError: plain error']);
362
+
363
+ function ErroredApp() {
364
+ return <span>loading</span>;
365
+ }
366
+
367
+ // Reset test environment
368
+ buffer = '';
369
+ hasErrored = false;
370
+ writable = new Stream.PassThrough();
371
+ writable.setEncoding('utf8');
372
+ writable.on('data', chunk => {
373
+ buffer += chunk;
374
+ });
375
+ writable.on('error', error => {
376
+ hasErrored = true;
377
+ fatalError = error;
378
+ });
379
+
380
+ // The Server errored at the shell. The recommended approach is to render a
381
+ // fallback loading state, which can then be hydrated with a mismatch.
382
+ await serverAct(async () => {
383
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<ErroredApp />);
384
+ pipe(writable);
385
+ });
386
+
387
+ expect(container.innerHTML).toBe('<span>loading</span>');
388
+
389
+ // Hydration suspends because the data for the shell hasn't loaded yet
390
+ await clientAct(async () => {
391
+ ReactDOMClient.hydrateRoot(container, <App />, {
392
+ onCaughtError(error) {
393
+ Scheduler.log('onCaughtError: ' + error.message);
394
+ },
395
+ onUncaughtError(error) {
396
+ Scheduler.log('onUncaughtError: ' + error.message);
397
+ },
398
+ onRecoverableError(error) {
399
+ Scheduler.log('onRecoverableError: ' + error.message);
400
+ },
401
+ });
402
+ });
403
+
404
+ assertLog(['onCaughtError: plain error']);
405
+ expect(container.textContent).toBe('Caught an error: plain error');
406
+ });
407
+
408
+ it('client renders when a client error is thrown in an error boundary', async () => {
409
+ let isClient = false;
410
+
411
+ function Throws() {
412
+ if (isClient) {
413
+ throw new Error('plain error');
414
+ }
415
+ return <div>Hello world</div>;
416
+ }
417
+
418
+ class ErrorBoundary extends React.Component {
419
+ state = {error: null};
420
+ static getDerivedStateFromError(error) {
421
+ return {error};
422
+ }
423
+ render() {
424
+ if (this.state.error) {
425
+ return <div>Caught an error: {this.state.error.message}</div>;
426
+ }
427
+ return this.props.children;
428
+ }
429
+ }
430
+
431
+ function App() {
432
+ return (
433
+ <ErrorBoundary>
434
+ <Throws />
435
+ </ErrorBoundary>
436
+ );
437
+ }
438
+
439
+ // Server render
440
+ await serverAct(async () => {
441
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
442
+ onError(error) {
443
+ Scheduler.log('onError: ' + error.message);
444
+ },
445
+ });
446
+ pipe(writable);
447
+ });
448
+ assertLog([]);
449
+
450
+ expect(container.innerHTML).toBe('<div>Hello world</div>');
451
+
452
+ isClient = true;
453
+
454
+ // Hydration suspends because the data for the shell hasn't loaded yet
455
+ await clientAct(async () => {
456
+ ReactDOMClient.hydrateRoot(container, <App />, {
457
+ onCaughtError(error) {
458
+ Scheduler.log('onCaughtError: ' + error.message);
459
+ },
460
+ onUncaughtError(error) {
461
+ Scheduler.log('onUncaughtError: ' + error.message);
462
+ },
463
+ onRecoverableError(error) {
464
+ Scheduler.log('onRecoverableError: ' + error.message);
465
+ },
466
+ });
467
+ });
468
+
469
+ assertLog(['onCaughtError: plain error']);
470
+ expect(container.textContent).toBe('Caught an error: plain error');
471
+ });
472
+
473
+ it('client renders when a hydration pass error is thrown in an error boundary', async () => {
474
+ let isClient = false;
475
+ let isFirst = true;
476
+
477
+ function Throws() {
478
+ if (isClient && isFirst) {
479
+ isFirst = false; // simulate a hydration or concurrent error
480
+ throw new Error('plain error');
481
+ }
482
+ return <div>Hello world</div>;
483
+ }
484
+
485
+ class ErrorBoundary extends React.Component {
486
+ state = {error: null};
487
+ static getDerivedStateFromError(error) {
488
+ return {error};
489
+ }
490
+ render() {
491
+ if (this.state.error) {
492
+ return <div>Caught an error: {this.state.error.message}</div>;
493
+ }
494
+ return this.props.children;
495
+ }
496
+ }
497
+
498
+ function App() {
499
+ return (
500
+ <ErrorBoundary>
501
+ <Throws />
502
+ </ErrorBoundary>
503
+ );
504
+ }
505
+
506
+ // Server render
507
+ await serverAct(async () => {
508
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
509
+ onError(error) {
510
+ Scheduler.log('onError: ' + error.message);
511
+ },
512
+ });
513
+ pipe(writable);
514
+ });
515
+ assertLog([]);
516
+
517
+ expect(container.innerHTML).toBe('<div>Hello world</div>');
518
+
519
+ isClient = true;
520
+
521
+ // Hydration suspends because the data for the shell hasn't loaded yet
522
+ await clientAct(async () => {
523
+ ReactDOMClient.hydrateRoot(container, <App />, {
524
+ onCaughtError(error) {
525
+ Scheduler.log('onCaughtError: ' + error.message);
526
+ },
527
+ onUncaughtError(error) {
528
+ Scheduler.log('onUncaughtError: ' + error.message);
529
+ },
530
+ onRecoverableError(error) {
531
+ Scheduler.log('onRecoverableError: ' + error.message);
532
+ },
533
+ });
534
+ });
535
+
536
+ assertLog([
537
+ 'onRecoverableError: plain error',
538
+ 'onRecoverableError: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
539
+ ]);
540
+ expect(container.textContent).toBe('Hello world');
541
+ });
542
});