Merge changes Id0f7deac,I27b6be10,Id24991b4,Ibcac4ab0,I8c8d581f, ...

* changes:
  Remove errFn from save(|Diff|Edit)Preferences
  Remove errFn from getGroupMembers
  Remove errFn from createRepoBranch and createRepoTag
  Remove errFn from deleteRepoTags
  Remove errFn from deleteRepoBranches
  Remove errFn from createGroup
  Remove errFn from createRepo
  Remove errFn from saveRepoConfig and runRepoGC
  Create proper event and util for network-error
  Create proper event and util for server-error
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
index 13bd13b..0145b9f 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
@@ -128,6 +128,7 @@
   }
 
   _handleRunningGC() {
+    if (!this.repo) return;
     this._runningGC = true;
     return this.restApiService
       .runRepoGC(this.repo)
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 085aa27..6301920 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -106,7 +106,7 @@
 import {isAttentionSetEnabled} from '../../../utils/attention-set-util';
 import {CODE_REVIEW, getMaxAccounts} from '../../../utils/label-util';
 import {isUnresolved} from '../../../utils/comment-util';
-import {fireAlert} from '../../../utils/event-util';
+import {fireAlert, fireServerError} from '../../../utils/event-util';
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 
@@ -721,13 +721,7 @@
           return new Map<AccountId | EmailAddress, boolean>();
         }
         if (!response.ok) {
-          this.dispatchEvent(
-            new CustomEvent('server-error', {
-              detail: {response},
-              composed: true,
-              bubbles: true,
-            })
-          );
+          fireServerError(response);
           return new Map<AccountId | EmailAddress, boolean>();
         }
 
@@ -790,8 +784,9 @@
     return account._account_id === change.owner._account_id;
   }
 
-  _handle400Error(response?: Response | null) {
-    if (!response) throw new Error('Reponse is empty.');
+  _handle400Error(r?: Response | null) {
+    if (!r) throw new Error('Reponse is empty.');
+    let response: Response = r;
     // A call to _saveReview could fail with a server error if erroneous
     // reviewers were requested. This is signalled with a 400 Bad Request
     // status. The default gr-rest-api-interface error handling would
@@ -813,7 +808,7 @@
       const result = parsed as ReviewResult;
       // Only perform custom error handling for 400s and a parseable
       // ReviewResult response.
-      if (response && response.status === 400 && result && result.reviewers) {
+      if (response.status === 400 && result && result.reviewers) {
         const errors: string[] = [];
         const addReviewers = Object.values(result.reviewers);
         addReviewers.forEach(r => errors.push(r.error ?? 'no explanation'));
@@ -823,13 +818,7 @@
           text: () => Promise.resolve(errors.join(', ')),
         };
       }
-      this.dispatchEvent(
-        new CustomEvent('server-error', {
-          detail: {response},
-          composed: true,
-          bubbles: true,
-        })
-      );
+      fireServerError(response);
     });
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index a606624..3379cd4 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -21,6 +21,7 @@
 import {mockPromise} from '../../../test/test-utils.js';
 import {SpecialFilePath} from '../../../constants/constants.js';
 import {appContext} from '../../../services/app-context.js';
+import {addListenerForTest} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-reply-dialog');
 
@@ -778,23 +779,23 @@
     assert.isTrue(eraseDraftCommentStub.calledWith(location));
   });
 
-  test('400 converts to human-readable server-error', async () => {
+  test('400 converts to human-readable server-error', done => {
     sinon.stub(window, 'fetch').callsFake(() => {
       const text = '....{"reviewers":{"id1":{"error":"human readable"}}}';
       return Promise.resolve(cloneableResponse(400, text));
     });
 
-    let resolver;
-    const promise = new Promise(r => resolver = r);
-    element.addEventListener('server-error', resolver);
+    const listener = event => {
+      if (event.target !== document) return;
+      event.detail.response.text().then(body => {
+        if (body === 'human readable') {
+          done();
+        }
+      });
+    };
+    addListenerForTest(document, 'server-error', listener);
 
-    await flush();
-    element.send();
-
-    const event = await promise;
-    assert.equal(event.target, element);
-    const text = await event.detail.response.text();
-    assert.equal(text, 'human readable');
+    flush(() => { element.send(); });
   });
 
   test('non-json 400 is treated as a normal server-error', done => {
@@ -803,15 +804,15 @@
       return Promise.resolve(cloneableResponse(400, text));
     });
 
-    element.addEventListener('server-error', event => {
-      if (event.target !== element) {
-        return;
-      }
+    const listener = event => {
+      if (event.target !== document) return;
       event.detail.response.text().then(body => {
-        assert.equal(body, 'Comment validation error!');
-        done();
+        if (body === 'Comment validation error!') {
+          done();
+        }
       });
-    });
+    };
+    addListenerForTest(document, 'server-error', listener);
 
     // Async tick is needed because iron-selector content is distributed and
     // distributed content requires an observer to be set up.
@@ -1255,12 +1256,13 @@
         },
       },
     });
-    element.addEventListener('server-error', e => {
+    const listener = e => {
       e.detail.response.text().then(text => {
         assert.equal(text, [error1, error2, error3].join(', '));
         done();
       });
-    });
+    };
+    addListenerForTest(document, 'server-error', listener);
     element._handle400Error(cloneableResponse(400, text));
   });
 
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index 4aa239a..e868f03 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -34,10 +34,10 @@
 import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
 import {GrErrorDialog} from '../gr-error-dialog/gr-error-dialog';
 import {GrAlert} from '../../shared/gr-alert/gr-alert';
-import {FetchRequest} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
 import {ErrorType, FixIronA11yAnnouncer} from '../../../types/types';
 import {AccountId} from '../../../types/common';
 import {EventType} from '../../../utils/event-util';
+import {NetworkErrorEvent, ServerErrorEvent} from '../../../types/events';
 
 const HIDE_ALERT_TIMEOUT_MS = 5000;
 const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
@@ -125,8 +125,8 @@
   /** @override */
   attached() {
     super.attached();
-    this.listen(document, 'server-error', '_handleServerError');
-    this.listen(document, 'network-error', '_handleNetworkError');
+    this.listen(document, EventType.SERVER_ERROR, '_handleServerError');
+    this.listen(document, EventType.NETWORK_ERROR, '_handleNetworkError');
     this.listen(document, EventType.SHOW_ALERT, '_handleShowAlert');
     this.listen(document, 'hide-alert', '_hideAlert');
     this.listen(document, 'show-error', '_handleShowErrorDialog');
@@ -147,8 +147,8 @@
   detached() {
     super.detached();
     this._clearHideAlertHandle();
-    this.unlisten(document, 'server-error', '_handleServerError');
-    this.unlisten(document, 'network-error', '_handleNetworkError');
+    this.unlisten(document, EventType.SERVER_ERROR, '_handleServerError');
+    this.unlisten(document, EventType.NETWORK_ERROR, '_handleNetworkError');
     this.unlisten(document, EventType.SHOW_ALERT, '_handleShowAlert');
     this.unlisten(document, 'hide-alert', '_hideAlert');
     this.unlisten(document, 'show-error', '_handleShowErrorDialog');
@@ -177,9 +177,7 @@
     });
   }
 
-  _handleServerError(
-    e: CustomEvent<{response: Response; request: FetchRequest}>
-  ) {
+  _handleServerError(e: ServerErrorEvent) {
     const {request, response} = e.detail;
     response.text().then(errorText => {
       const url = request && (request.anonymizedUrl || request.url);
@@ -296,7 +294,7 @@
     );
   }
 
-  _handleNetworkError(e: CustomEvent) {
+  _handleNetworkError(e: NetworkErrorEvent) {
     this._showAlert('Server unavailable');
     console.error(e.detail.error.message);
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index a267b0b..3c401d0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -68,7 +68,11 @@
 import {LineNumber, FILE} from '../gr-diff/gr-diff-line';
 import {GrCommentThread} from '../../shared/gr-comment-thread/gr-comment-thread';
 import {KnownExperimentId} from '../../../services/flags/flags';
-import {firePageError, fireAlert} from '../../../utils/event-util';
+import {
+  firePageError,
+  fireAlert,
+  fireServerError,
+} from '../../../utils/event-util';
 
 const MSG_EMPTY_BLAME = 'No blame information for this diff.';
 
@@ -591,13 +595,7 @@
     // Loading the diff may respond with 409 if the file is too large. In this
     // case, use a toast error..
     if (response.status === 409) {
-      this.dispatchEvent(
-        new CustomEvent('server-error', {
-          detail: {response},
-          composed: true,
-          bubbles: true,
-        })
-      );
+      fireServerError(response);
       return;
     }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 81a3604..9bde122 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -304,11 +304,15 @@
 
       setup(() => {
         serverErrorStub = sinon.stub();
-        element.addEventListener('server-error', serverErrorStub);
+        document.addEventListener('server-error', serverErrorStub);
         pageErrorStub = sinon.stub();
         element.addEventListener('page-error', pageErrorStub);
       });
 
+      teardown(() => {
+        document.removeEventListener('server-error', serverErrorStub);
+      });
+
       test('page error on HTTP-409', () => {
         element._handleGetDiffError({status: 409});
         assert.isTrue(serverErrorStub.calledOnce);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
index edb0113..1c6e0b6 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
@@ -157,7 +157,7 @@
   HttpMethod,
   ReviewerState,
 } from '../../../constants/constants';
-import {firePageError} from '../../../utils/event-util';
+import {firePageError, fireServerError} from '../../../utils/event-util';
 
 const JSON_PREFIX = ")]}'";
 const MAX_PROJECT_RESULTS = 25;
@@ -408,19 +408,7 @@
     }) as Promise<DashboardInfo[] | undefined>;
   }
 
-  saveRepoConfig(repo: RepoName, config: ConfigInput): Promise<Response>;
-
-  saveRepoConfig(
-    repo: RepoName,
-    config: ConfigInput,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
-  saveRepoConfig(
-    repo: RepoName,
-    config: ConfigInput,
-    errFn?: ErrorCallback
-  ): Promise<Response | undefined> {
+  saveRepoConfig(repo: RepoName, config: ConfigInput): Promise<Response> {
     // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
     // supports it.
     const url = `/projects/${encodeURIComponent(repo)}/config`;
@@ -429,23 +417,11 @@
       method: HttpMethod.PUT,
       url,
       body: config,
-      errFn,
       anonymizedUrl: '/projects/*/config',
     });
   }
 
-  runRepoGC(repo: RepoName): Promise<Response>;
-
-  runRepoGC(
-    repo: RepoName,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
-  runRepoGC(repo: RepoName, errFn?: ErrorCallback) {
-    if (!repo) {
-      // TODO(TS): fix return value
-      return '';
-    }
+  runRepoGC(repo: RepoName): Promise<Response> {
     // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
     // supports it.
     const encodeName = encodeURIComponent(repo);
@@ -453,23 +429,11 @@
       method: HttpMethod.POST,
       url: `/projects/${encodeName}/gc`,
       body: '',
-      errFn,
       anonymizedUrl: '/projects/*/gc',
     });
   }
 
-  createRepo(config: ProjectInput & {name: RepoName}): Promise<Response>;
-
-  createRepo(
-    config: ProjectInput & {name: RepoName},
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
-  createRepo(config: ProjectInput, errFn?: ErrorCallback) {
-    if (!config.name) {
-      // TODO(TS): Fix return value
-      return '';
-    }
+  createRepo(config: ProjectInput & {name: RepoName}): Promise<Response> {
     // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
     // supports it.
     const encodeName = encodeURIComponent(config.name);
@@ -477,29 +441,16 @@
       method: HttpMethod.PUT,
       url: `/projects/${encodeName}`,
       body: config,
-      errFn,
       anonymizedUrl: '/projects/*',
     });
   }
 
-  createGroup(config: GroupInput & {name: string}): Promise<Response>;
-
-  createGroup(
-    config: GroupInput & {name: string},
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
-  createGroup(config: GroupInput, errFn?: ErrorCallback) {
-    if (!config.name) {
-      // TODO(TS): Fix return value
-      return '';
-    }
+  createGroup(config: GroupInput & {name: string}): Promise<Response> {
     const encodeName = encodeURIComponent(config.name);
     return this._restApiHelper.send({
       method: HttpMethod.PUT,
       url: `/groups/${encodeName}`,
       body: config,
-      errFn,
       anonymizedUrl: '/groups/*',
     });
   }
@@ -515,19 +466,7 @@
     }) as Promise<GroupInfo | undefined>;
   }
 
-  deleteRepoBranches(repo: RepoName, ref: GitRef): Promise<Response>;
-
-  deleteRepoBranches(
-    repo: RepoName,
-    ref: GitRef,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
-  deleteRepoBranches(repo: RepoName, ref: GitRef, errFn?: ErrorCallback) {
-    if (!repo || !ref) {
-      // TODO(TS): fix return value
-      return '';
-    }
+  deleteRepoBranches(repo: RepoName, ref: GitRef): Promise<Response> {
     // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
     // supports it.
     const encodeName = encodeURIComponent(repo);
@@ -536,24 +475,11 @@
       method: HttpMethod.DELETE,
       url: `/projects/${encodeName}/branches/${encodeRef}`,
       body: '',
-      errFn,
       anonymizedUrl: '/projects/*/branches/*',
     });
   }
 
-  deleteRepoTags(repo: RepoName, ref: GitRef): Promise<Response>;
-
-  deleteRepoTags(
-    repo: RepoName,
-    ref: GitRef,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
-  deleteRepoTags(repo: RepoName, ref: GitRef, errFn?: ErrorCallback) {
-    if (!repo || !ref) {
-      // TODO(TS): fix return type
-      return '';
-    }
+  deleteRepoTags(repo: RepoName, ref: GitRef): Promise<Response> {
     // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
     // supports it.
     const encodeName = encodeURIComponent(repo);
@@ -562,7 +488,6 @@
       method: HttpMethod.DELETE,
       url: `/projects/${encodeName}/tags/${encodeRef}`,
       body: '',
-      errFn,
       anonymizedUrl: '/projects/*/tags/*',
     });
   }
@@ -571,25 +496,7 @@
     name: RepoName,
     branch: BranchName,
     revision: BranchInput
-  ): Promise<Response>;
-
-  createRepoBranch(
-    name: RepoName,
-    branch: BranchName,
-    revision: BranchInput,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
-  createRepoBranch(
-    name: RepoName,
-    branch: BranchName,
-    revision: BranchInput,
-    errFn?: ErrorCallback
-  ) {
-    if (!name || !branch || !revision) {
-      // TODO(TS) fix return type
-      return '';
-    }
+  ): Promise<Response> {
     // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
     // supports it.
     const encodeName = encodeURIComponent(name);
@@ -598,7 +505,6 @@
       method: HttpMethod.PUT,
       url: `/projects/${encodeName}/branches/${encodeBranch}`,
       body: revision,
-      errFn,
       anonymizedUrl: '/projects/*/branches/*',
     });
   }
@@ -607,25 +513,7 @@
     name: RepoName,
     tag: string,
     revision: TagInput
-  ): Promise<Response>;
-
-  createRepoTag(
-    name: RepoName,
-    tag: string,
-    revision: TagInput,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
-  createRepoTag(
-    name: RepoName,
-    tag: string,
-    revision: TagInput,
-    errFn?: ErrorCallback
-  ) {
-    if (!name || !tag || !revision) {
-      // TODO(TS): Fix return value
-      return '';
-    }
+  ): Promise<Response> {
     // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
     // supports it.
     const encodeName = encodeURIComponent(name);
@@ -634,7 +522,6 @@
       method: HttpMethod.PUT,
       url: `/projects/${encodeName}/tags/${encodeTag}`,
       body: revision,
-      errFn,
       anonymizedUrl: '/projects/*/tags/*',
     });
   }
@@ -650,16 +537,12 @@
     );
   }
 
-  getGroupMembers(
-    groupName: GroupId | GroupName,
-    errFn?: ErrorCallback
-  ): Promise<AccountInfo[] | undefined> {
+  getGroupMembers(groupName: GroupId | GroupName): Promise<AccountInfo[]> {
     const encodeName = encodeURIComponent(groupName);
-    return this._restApiHelper.fetchJSON({
+    return (this._restApiHelper.fetchJSON({
       url: `/groups/${encodeName}/members/`,
-      errFn,
       anonymizedUrl: '/groups/*/members',
-    }) as Promise<AccountInfo[] | undefined>;
+    }) as unknown) as Promise<AccountInfo[]>;
   }
 
   getIncludedGroup(
@@ -865,14 +748,7 @@
     });
   }
 
-  savePreferences(prefs: PreferencesInput): Promise<Response>;
-
-  savePreferences(
-    prefs: PreferencesInput,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
-  savePreferences(prefs: PreferencesInput, errFn?: ErrorCallback) {
+  savePreferences(prefs: PreferencesInput): Promise<Response> {
     // Note (Issue 5142): normalize the download scheme with lower case before
     // saving.
     if (prefs.download_scheme) {
@@ -883,45 +759,28 @@
       method: HttpMethod.PUT,
       url: '/accounts/self/preferences',
       body: prefs,
-      errFn,
       reportUrlAsIs: true,
     });
   }
 
-  saveDiffPreferences(prefs: DiffPreferenceInput): Promise<Response>;
-
-  saveDiffPreferences(
-    prefs: DiffPreferenceInput,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
-  saveDiffPreferences(prefs: DiffPreferenceInput, errFn?: ErrorCallback) {
+  saveDiffPreferences(prefs: DiffPreferenceInput): Promise<Response> {
     // Invalidate the cache.
     this._cache.delete('/accounts/self/preferences.diff');
     return this._restApiHelper.send({
       method: HttpMethod.PUT,
       url: '/accounts/self/preferences.diff',
       body: prefs,
-      errFn,
       reportUrlAsIs: true,
     });
   }
 
-  saveEditPreferences(prefs: EditPreferencesInfo): Promise<Response>;
-
-  saveEditPreferences(
-    prefs: EditPreferencesInfo,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
-  saveEditPreferences(prefs: EditPreferencesInfo, errFn?: ErrorCallback) {
+  saveEditPreferences(prefs: EditPreferencesInfo): Promise<Response> {
     // Invalidate the cache.
     this._cache.delete('/accounts/self/preferences.edit');
     return this._restApiHelper.send({
       method: HttpMethod.PUT,
       url: '/accounts/self/preferences.edit',
       body: prefs,
-      errFn,
       reportUrlAsIs: true,
     });
   }
@@ -1494,13 +1353,7 @@
             if (errFn) {
               errFn.call(null, response);
             } else {
-              document.dispatchEvent(
-                new CustomEvent('server-error', {
-                  detail: {request: req, response},
-                  composed: true,
-                  bubbles: true,
-                })
-              );
+              fireServerError(response, req);
             }
             return undefined;
           }
@@ -2207,14 +2060,8 @@
     // 404s indicate the file does not exist yet in the revision, so suppress
     // them.
     const suppress404s: ErrorCallback = res => {
-      if (res?.status !== 404) {
-        document.dispatchEvent(
-          new CustomEvent('server-error', {
-            detail: {res},
-            composed: true,
-            bubbles: true,
-          })
-        );
+      if (res && res?.status !== 404) {
+        fireServerError(res);
       }
       return res;
     };
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
index bd2818c..4bc7dd3 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
@@ -1294,7 +1294,7 @@
         })
         .then(() => {
           assert.isTrue(spy.called);
-          assert.notEqual(spy.lastCall.args[0].detail.res.status, 404);
+          assert.notEqual(spy.lastCall.args[0].detail.response.status, 404);
         });
   });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
index 8aa1b0f..43a6af4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
@@ -32,6 +32,8 @@
 } from '../../../../types/common';
 import {HttpMethod} from '../../../../constants/constants';
 import {RpcLogEventDetail} from '../../../../types/events';
+import {fireNetworkError, fireServerError} from '../../../../utils/event-util';
+import {FetchRequest} from '../../../../types/types';
 
 const JSON_PREFIX = ")]}'";
 
@@ -188,12 +190,6 @@
 
 export type SendRequest = SendRawRequest | SendJSONRequest;
 
-export interface FetchRequest {
-  url: string;
-  fetchOptions?: AuthRequestInit;
-  anonymizedUrl?: string;
-}
-
 export interface FetchJSONRequest extends FetchRequest {
   reportUrlAsIs?: boolean;
   params?: FetchParams;
@@ -315,13 +311,7 @@
         if (req.errFn) {
           req.errFn.call(undefined, null, err);
         } else {
-          document.dispatchEvent(
-            new CustomEvent('network-error', {
-              detail: {error: err},
-              composed: true,
-              bubbles: true,
-            })
-          );
+          fireNetworkError(err);
         }
         throw err;
       });
@@ -350,13 +340,7 @@
           req.errFn.call(null, response);
           return;
         }
-        document.dispatchEvent(
-          new CustomEvent('server-error', {
-            detail: {request: req, response},
-            composed: true,
-            bubbles: true,
-          })
-        );
+        fireServerError(response, req);
         return;
       }
       return this.getResponseObject(response);
@@ -510,13 +494,7 @@
     };
     const xhr = this.fetch(fetchReq)
       .catch(err => {
-        document.dispatchEvent(
-          new CustomEvent('network-error', {
-            detail: {error: err},
-            composed: true,
-            bubbles: true,
-          })
-        );
+        fireNetworkError(err);
         if (req.errFn) {
           return req.errFn.call(undefined, null, err);
         } else {
@@ -529,13 +507,7 @@
             req.errFn.call(undefined, response);
             return;
           }
-          document.dispatchEvent(
-            new CustomEvent('server-error', {
-              detail: {request: fetchReq, response},
-              composed: true,
-              bubbles: true,
-            })
-          );
+          fireServerError(response, fetchReq);
         }
         return response;
       });
diff --git a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
index cf064c1..8a0f912 100644
--- a/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/services/gr-rest-api/gr-rest-api.ts
@@ -232,26 +232,10 @@
   getDiffPreferences(): Promise<DiffPreferencesInfo | undefined>;
 
   saveDiffPreferences(prefs: DiffPreferenceInput): Promise<Response>;
-  saveDiffPreferences(
-    prefs: DiffPreferenceInput,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-  saveDiffPreferences(
-    prefs: DiffPreferenceInput,
-    errFn?: ErrorCallback
-  ): Promise<Response>;
 
   getEditPreferences(): Promise<EditPreferencesInfo | undefined>;
 
   saveEditPreferences(prefs: EditPreferencesInfo): Promise<Response>;
-  saveEditPreferences(
-    prefs: EditPreferencesInfo,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-  saveEditPreferences(
-    prefs: EditPreferencesInfo,
-    errFn?: ErrorCallback
-  ): Promise<Response>;
 
   getAccountEmails(): Promise<EmailInfo[] | undefined>;
   deleteAccountEmail(email: string): Promise<Response>;
@@ -267,25 +251,11 @@
     revision: BranchInput
   ): Promise<Response>;
 
-  createRepoBranch(
-    name: RepoName,
-    branch: BranchName,
-    revision: BranchInput,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-
   createRepoTag(
     name: RepoName,
     tag: string,
     revision: TagInput
   ): Promise<Response>;
-
-  createRepoTag(
-    name: RepoName,
-    tag: string,
-    revision: TagInput,
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
   addAccountGPGKey(key: GpgKeysInput): Promise<Record<string, GpgKeyInfo>>;
   deleteAccountGPGKey(id: GpgKeyId): Promise<Response>;
   getAccountGPGKeys(): Promise<Record<string, GpgKeyInfo>>;
@@ -325,11 +295,6 @@
   ): Promise<ProjectAccessInfo | undefined>;
 
   createRepo(config: ProjectInput & {name: RepoName}): Promise<Response>;
-  createRepo(
-    config: ProjectInput & {name: RepoName},
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-  createRepo(config: ProjectInput, errFn?: ErrorCallback): Promise<Response>;
 
   getRepo(
     repo: RepoName,
@@ -522,11 +487,6 @@
     | Promise<PathToCommentsInfoMap | undefined>;
 
   createGroup(config: GroupInput & {name: string}): Promise<Response>;
-  createGroup(
-    config: GroupInput & {name: string},
-    errFn: ErrorCallback
-  ): Promise<Response | undefined>;
-  createGroup(config: GroupInput, errFn?: ErrorCallback): Promise<Response>;
 
   getPlugins(
     filter: string,
@@ -660,10 +620,7 @@
     errFn?: ErrorCallback
   ): Promise<GroupAuditEventInfo[] | undefined>;
 
-  getGroupMembers(
-    groupName: GroupId | GroupName,
-    errFn?: ErrorCallback
-  ): Promise<AccountInfo[] | undefined>;
+  getGroupMembers(groupName: GroupId | GroupName): Promise<AccountInfo[]>;
 
   getIncludedGroup(
     groupName: GroupId | GroupName
@@ -690,10 +647,7 @@
     includedGroup: GroupId
   ): Promise<Response>;
 
-  runRepoGC(
-    repo: RepoName,
-    errFn?: ErrorCallback
-  ): Promise<Response | undefined>;
+  runRepoGC(repo: RepoName): Promise<Response>;
   getFileContent(
     changeNum: NumericChangeId,
     path: string,
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index cccba5d..08642af 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -105,6 +105,25 @@
   cleanups.push(cleanupCallback);
 }
 
+export function addListenerForTest(
+  el: EventTarget,
+  type: string,
+  listener: EventListenerOrEventListenerObject
+) {
+  el.addEventListener(type, listener);
+  registerListenerCleanup(el, type, listener);
+}
+
+export function registerListenerCleanup(
+  el: EventTarget,
+  type: string,
+  listener: EventListenerOrEventListenerObject
+) {
+  registerTestCleanup(() => {
+    el.removeEventListener(type, listener);
+  });
+}
+
 export function cleanupTestUtils() {
   cleanups.forEach(cleanup => cleanup());
   cleanups.splice(0);
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index f136045..e0b3348 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -19,6 +19,7 @@
 import {UIComment} from '../utils/comment-util';
 import {Side} from '../constants/constants';
 import {LineNumber} from '../elements/diff/gr-diff/gr-diff-line';
+import {FetchRequest} from './types';
 
 export interface TitleChangeEventDetail {
   title: string;
@@ -44,6 +45,31 @@
   }
 }
 
+export interface ServerErrorEventDetail {
+  request?: FetchRequest;
+  response: Response;
+}
+
+export type ServerErrorEvent = CustomEvent<ServerErrorEventDetail>;
+
+declare global {
+  interface DocumentEventMap {
+    'server-error': ServerErrorEvent;
+  }
+}
+
+export interface NetworkErrorEventDetail {
+  error: Error;
+}
+
+export type NetworkErrorEvent = CustomEvent<NetworkErrorEventDetail>;
+
+declare global {
+  interface DocumentEventMap {
+    'network-error': NetworkErrorEvent;
+  }
+}
+
 export interface LocationChangeEventDetail {
   hash: string;
   pathname: string;
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index b40d618..232e204 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -27,6 +27,7 @@
   PatchSetNum,
 } from './common';
 import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
+import {AuthRequestInit} from '../services/gr-auth/gr-auth';
 
 export function notUndefined<T>(x: T | undefined): x is T {
   return x !== undefined;
@@ -237,3 +238,9 @@
 >(x: T | PolymerSpliceChange<U>): x is PolymerSpliceChange<U> {
   return (x as PolymerSpliceChange<U>).indexSplices !== undefined;
 }
+
+export interface FetchRequest {
+  url: string;
+  fetchOptions?: AuthRequestInit;
+  anonymizedUrl?: string;
+}
diff --git a/polygerrit-ui/app/utils/event-util.ts b/polygerrit-ui/app/utils/event-util.ts
index 0af8fe2..36341bd 100644
--- a/polygerrit-ui/app/utils/event-util.ts
+++ b/polygerrit-ui/app/utils/event-util.ts
@@ -15,9 +15,13 @@
  * limitations under the License.
  */
 
+import {FetchRequest} from '../types/types';
+
 export enum EventType {
   SHOW_ALERT = 'show-alert',
   PAGE_ERROR = 'page-error',
+  SERVER_ERROR = 'server-error',
+  NETWORK_ERROR = 'network-error',
   TITLE_CHANGE = 'title-change',
 }
 
@@ -41,6 +45,26 @@
   );
 }
 
+export function fireServerError(response: Response, request?: FetchRequest) {
+  document.dispatchEvent(
+    new CustomEvent(EventType.SERVER_ERROR, {
+      detail: {response, request},
+      composed: true,
+      bubbles: true,
+    })
+  );
+}
+
+export function fireNetworkError(error: Error) {
+  document.dispatchEvent(
+    new CustomEvent(EventType.NETWORK_ERROR, {
+      detail: {error},
+      composed: true,
+      bubbles: true,
+    })
+  );
+}
+
 export function fireTitleChange(target: EventTarget, title: string) {
   target.dispatchEvent(
     new CustomEvent(EventType.TITLE_CHANGE, {