My Angular5 app loads a config file from the backend during application initialization (APP_INITIALIZER). Since the app cannot be run without it, my goal is to show a message to the user that the config couldn't be loaded.
providers: [ AppConfig,
{ provide: APP_INITIALIZER, useFactory: (config: AppConfig) => () => config.load(), deps: [AppConfig], multi: true },
{ provide: LocationStrategy, useClass: HashLocationStrategy},
{ provide: ErrorHandler, useClass: GlobalErrorHandler }]
The AppConfig
class should load a config file from a backend service before the app loads:
@Injectable()
export class AppConfig {
private config: Object = null;
constructor(private http: HttpClient) {}
public getConfig(key: any) {
return this.config[key];
}
public load() {
return new Promise((resolve, reject) => {
this.http.get(environment.serviceUrl + 'config/config')
.catch((error: any) => {
return Observable.throw(error || 'Server error');
})
.subscribe((responseData) => {
this.config = responseData;
this.config['service_endpoint'] = environment.serviceUrl;
resolve(true);
});
});
}
}
Global Exception handler:
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
constructor( private messageService: MessageService) { }
handleError(error) {
// the AppConfig exception cannot be shown with the growl message since the growl component is within the AppComponent
this.messageService.add({severity: 'error', summary: 'Exception', detail: `Global Exception Handler: ${error.message}`});
throw error;
}
}
If the config file cannot be loaded, an exception is thrown, caught & rethrown in the global exception handler (uncaught HTTPErrorResponse in console.log()) and the loading spinner is hanging for ever)
Since the AppComponent
does not get loaded (which is ok, since the app cannot be used without the configuration) and as my message/"growl" Component is a sub component of the AppComponent
, I cannot display a message to the user.
Is there a way to show a message in the index.html
page during this stage? I wouldn't like to redirect the user to a different .html page than the index.html, since the users will then just reload/f5 on the error.html.
In main.ts I have changed bootstrap in this way:
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => {
// error should be logged into console by defaultErrorLogger, so no extra logging is necessary here
// console.log(err);
// show error to user
const errorMsgElement = document.querySelector('#errorMsgElement');
let message = 'Application initialization failed';
if (err) {
if (err.message) {
message = message + ': ' + err.message;
} else {
message = message + ': ' + err;
}
}
errorMsgElement.textContent = message;
});
any occurred exceptions are caught and message is set into an html element. The element is defined in index.html in app-root tag, e.g.
<app-root><div id="errorMsgElement" style="padding: 20% 0; text-align: center;"></div>
</app-root>
The content of tag is replaced by angular, if bootstrap has been successful.
I haven't found a good solution for a very similar issue too, but as a workaround, this is how I did it:
Extend the config class with an initialized
variable:
@Injectable()
export class AppConfig {
public settings: AppSettings = null;
public initialized = false;
public load() {
return new Promise((resolve, reject) => {
this.http.get(environment.serviceUrl + 'config')
.catch((error: any) => {
this.initialized = false;
resolve(error);
return Observable.throw(error || 'Server error');
})
.subscribe((responseData: any) => {
this.settings = responseData;
this.initialized = true;
resolve(true);
});
});
}
}
And within the app.component.html
I show either the app or an error message, depending on this variable:
<div *ngIf="!appConfig.initialized">
<b>Failed to load config!</b>
<!-- or <app-config-error-page> -->
</div>
<div *ngIf="appConfig.initialized" #layoutContainer >
<!-- the app content -->
</div>
The global exception handler:
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
constructor( private notificationService: NotificationService) { }
handleError(error) {
this.notificationService.error('Exception', `${error.message}`);
return Observable.throw(error);
}
}
Of course one could also store the concrete error message / exception text in the config object and show it to the users in the app.component/error.page.component if needed.
I have a slightly different approach to offer. In my opinion, we do not want the bootstrap of the Angular app to continue if we can't properly initialize it, since it won't work properly. Instead, we just want to display message to the user, and stop.
Background: in my initialization code, I have to make two asynchronous requests in a particular order:
I make an HTTP request to my "own" web server (where the Angular app is served from), to get the hostname of second web server that hosts my back-end API.
I then make a request to that second web server to retrieve further configuration information.
If either of those requests fail, then I need to stop and show a message rather than Angular continuing with the bootstrap process.
Here's a simplified version of my code before adding in the new exception handling (note that I prefer to use Promise
and async
/await
rather than Observable
, but that's not really relevant):
const appInitializer = (
localConfigurationService: LocalConfigurationService,
remoteConfigurationService: RemoteConfigurationService
): () => Promise<void> => {
return async () => {
try {
const apiHostName = await localConfigurationService.getApiHostName();
await remoteConfigurationService.load(apiHostName);
} catch (e) {
console.error("Failed to initialize the Angular app!", e);
}
};
export const appInitializerProvider = {
provide: APP_INITIALIZER,
useFactory: appInitializer,
deps: [LocalConfigurationService, RemoteConfigurationService],
multi: true
};
That code will log the error to the console, but then continue with the bootstrap process - not what we want.
To modify this code to (a) display a message, and (b) stop the bootstrap process in its tracks, I added the following 3 lines immediately after that console.error
call:
window.document.body.classList.add('failed');
const forever = new Promise(() => { }); // will never resolve
await forever;
The first line simply adds the class 'failed' to the document <body>
element. We'll see what effect that has in a moment.
The other two lines await
a Promise
that is never resolved - in other words, they wait forever. This has the effect of stalling the Angular bootstrap process, so the Angular app never appears.
The final change is in my index.html
file (the HTML file that's loaded by the browser, and which "contains" the Angular app). Here's what it looked like before my changes:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My App</title>
<base href="/">
</head>
<body>
<app-root>Loading...</app-root>
</body>
</html>
That "loading..." text within the <app-root>
element is displayed while the Angular app is initialized, and is replaced with the app content once the bootstrap process is complete.
To ensure that an "oops!" message is shown if the app cannot be initialized, I've added a <style>
block with some CSS styles, and some extra content within the <app-root>
element:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My App</title>
<base href="/">
<style type="text/css">
#failure-message {
display: none;
}
body.failed #loading-message {
display: none;
}
body.failed #failure-message {
display: block;
}
</style>
</head>
<body>
<app-root>
<h1 id="loading-message">Loading...</h1>
<h1 id="failure-message">Oops! Something went wrong.</h1>
</app-root>
</body>
</html>
So now, the "loading..." message is displayed by default (to be replaced with the app content if initialization is successful), but the "oops!" message is displayed instead if the <body>
element has the failed
class - which is what we add in our exception handler.
Of course, you can do better than a simple <h1>
tag - you can put any content you like in there.
The net effect of all this is that the browser displays "loading..." and then either the Angular app loads or the "loading..." text is replaced with "oops!".
Note that the URL isn't changed, so if you hit reload in the browser, it'll start from scratch and try to load the Angular app all over again - which is probably what you want.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With