anchorError handling
SvelteKit's model is different by design. No zone, no DI, no interception layer. Errors are caught at whatever boundary happens to enclose them, and each boundary has its own recovery policy.
What you can't do is catch error and keep the page. There's no configuration where SvelteKit reports the error and leaves the tree standing. Angular's ErrorHandler does that; Svelte decided a component that threw an error shouldn't keep rendering.
anchorSvelte Error handling
This causes a bit of fragmentation in Svelte, especially with the fine tuning of error boundaries in templates. Here are the different levels of error handling in Svelte:
- starts with
boundary(the only error handler that does not throw back a redirect) - load functions returned "error" (Svelte built in object)
- client hook handles client-side errors
- server hook handles routing, template errors and server-side exceptions
- create
+error.sveltein the right place to catch the error (for logging issues, or reversal ... etc), these files can be placed anywhere in a tree of a route, to catch errors within context of that branch - finally
error.htmlitself catches everything else - Also, add window event listeners and server event listeners to catch errors the leftovers
In template boundaries:
<!--in your +page.svelte -->
<svelte:boundary>
normal template content
{#snippet failed(error, reset)}
oh oh, stay put, do not redirect to +error.svelte
this is the only hanlder that stays put
{/snippet}
</svelte:boundary>In load functions:
// +page.ts
export const load: PageLoad = async ({params}) => {
const order = await GetOrder({id: params.id});
if (!order){
// throw a native svelte error that is cauthgt by +error or error.html
error(404, {message: 'Order not found'});
}
// ...
}In client and server error handling hooks
// hooks.client.ts
export const handleError: HandleClientError = async (error) => {
// catch errors thrown from template rendering that are not caught
// this also catches unhandled error in case ssr is off, or server handler did not catch error
// this returns +error.svelte if exists, or error.html
// error.event has the route and url
const _error = (<any>error?.error)?.message || error?.message || error;
return {
message: _error || 'ERROR',
source: error.event?.url.pathname
};
};
// hooks.server.ts
export const handleError: HandleServerError = async (error) => {
// catch errors on server rendering, that are not handled
// if this is handled here, client hook does not handle it
// returns +error.svelte if found, or error.html
// error.event has the request
const _error = (<any>error?.error)?.message || error?.message || error;
return {
message: _error || 'UNKNOWN ERROR',
source: error.event?.request?.url
};
};The +error.svelte has access to error from the page object. And error.html through %sveltekit.error% directive.
// +error.svelte
import { page } from '$app/state';
// read page.error object if found
console.log(page?.status, page.error?.message, page.error?.source);
// error.html
// no access except to %sveltekit.status%, and %sveltekit.error.message%If it reaches the last page, it may look like a severe error, but honestly it’s just one error that you forgot to handle properly. Svelte could be a total drama queen!
API errors are caught in their fetch handler, or Axios interceptor, or {#await} in templates. But you need to clear out the error and never throw it back, unless you intend to try catch it locally (in template). In fetch you just check response.ok (Axios interception model can be recreated using Svelte `fetch` but I am too lazy to reinvent such a wheel!)
Of course, this is assuming you never make an explicithttprequest in a.sveltefile. And that you are not Claude.
// axios http.interceptor.ts
export const HttpInterceptor = (httpClient: AxiosInstance) => {
httpClient.interceptors.response.use(
null,
function (error) {
// pass a custom axios context to decide whether to throw back or handle
const _context = error.response?.config?.errorContext
if (_context.throwBack) {
return Promise.reject(error);
} else {
return Promise.resolve(error);
}
});
}
// in axiost.d.ts, delcare the extra context
import 'axios';
declare module 'axios' {
export interface AxiosRequestConfig {
errorContext?: {
throwBack?: boolean;
};
}
}
// then pass throwBack to http requests to force a throw back, to catch in calling template
await httpClient.post('url', data, {
errorContext: {
// let template catch it
throwBack: true
}
});Finally, there are two extra locations to fine tune error handling, the +layout.svelte, and in your custom Node server,
// +layout.svelte
onMount(() => {
window.addEventListener('error', (e) => {
console.log(e);
});
window.addEventListener('unhandledrejection', (e)=>{
console.log(e);
});
});In your Node server:
// index.js before everything
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
});
process.on('uncaughtException', (err, origin) => {
console.error('Unhandled Exception:', err);
});anchorAngular error handling
In Angular, here are the ways to handle errors, which looks similar to Svelte. The boundary is a new addition that acts like Svlete boundary.
- template
@deferto catch template errors locally - template
@boundary(still in preview for version 22) - global
ErrorHandlerimplementation (provided on bootstrap for all browser errors and unhandled rejection events) - routing errors are added directly to route config, navigation events can also be subscribed to at any level to fine tune error handling per route tree.
- Left overs on bootstrap level
When using Angular with SSR, Angular automatically adds the unhandledRejection and uncaughtException listeners to the server process. These handlers prevent the server from crashing and instead log captured errors to the console.
Template and rendering errors:
// error components can be positioned anywhere, where specific errors can be listered to
// in app.routes configuration file
const MyAppRoutes: Routes = [
{
path: '404',
component: NotFoundComponent,
},
// ... routes
{
// catch everything else
path: '**',
// either redirect to 404
// redirectTo: '/404',
// or show component in same url:
component: NotFoundComponent,
pathMatch: 'full',
}
},
// aslo subscribe to route events head start to catch and log
// provideEnvironmentInitializer(appFactory)
const appFactory = () => {
const router: Router = inject(Router);
router.events
.pipe(
filter(
(e) => e instanceof NavigationEnd
)
).subscribe((event) => {
if (event.urlAfterRedirects === '/404') {
if (event.url !== '/404') {
// log 404 error when 404 is redirected to
console.log('404', event.url);
}
}
});
// catch NavigationError
// this can also be done with the withNavigationErrorHandler token
router.events
.pipe(filter((e) => e instanceof NavigationError))
.subscribe((event) => {
// log unexepected navigation errors
console.log( 'unexpected error in navigation', event.error);
// redirect to '/error'
});
};
// route provider:
const AppRouteProviders = [
provideEnvironmentInitializer(appFactory),
provideRouter(
// routes array:
MyAppRoutes,
// catch navigation errors here instead (similar to above event subscriber)
withNavigationErrorHandler(errorFactory)
)
];
// Bootstrap error handler, main.ts
bootstrapApplication(AppComponent, {
providers: [
...AppRouteProviders,
// ask Angular to handle window unhandledrejection and error events
// via ErrorHandler
provideBrowserGlobalErrorListeners()
]
})
// the final left-overs
.catch((err) => console.log('the left-overs', err));API errors are caught with the http interceptor, using RxJS catchError operator. See the Http Client context we spoke of last week.
// http interceptor file
const SOMETHING = new HttpContextToken<any>(() => {throwBack: false});
// helper function
export const getErrorContext = (src: any) => {
return new HttpContext().set(SOMETHING, src);
};
export const OlInterceptorFn: HttpInterceptorFn = (req: HttpRequest<any>, next: HttpHandlerFn) => {
return next(adjustedReq)
.pipe(
// catch all http errors
catchError((e) => {
// if context rethrows
if (req.context.get(SOMETHING)) {
const throwBack = req.context.get(SOMETHING).throwBack;
// throw back or clear out
return throwBack ? throwError(() => e) : of(null);
}
// throw back
return throwError(() => e);
})
);
};
// example http call with throwback, let caller catch it
return this.http.post(Config.API.resource.query, _params, {
context: getErrorContext({throwBack: true})
});Conclusion: error handling acts differently in both frameworks, it is more fragmented in Svelte and leaves little room for fine tuning. Also there is manual work and definite missed gaps. The error.html final catch is an obscure dead end. In Angular again the boilerplate is larger, but it can all be handled in less files, and in the same folder, even if triggered on different routes, because routing is not file system based. In addition to the out of the box window and server error handlers that -I must admit- made me dumber!
anchorForms
Svelte does not get in the way of creating forms, it just provides the binding process, which is quite simple and works with their Runes $state perfectly well. Angular recently adopted the same concept where the form is just an interface, and the real work happens on a signal. But when you start working with validation, you realize you need to retrain your Svelte into understanding Web 101. I recreated the form input like I did in Angular, but the difference is obvious. In Svelte you're dealing with the DOM. No protection. In Angular you use their out of the box directives to enhance an existing form related directive.
anchorForm directive and validation
Here is an example in Angular for a form directive, that builds on top of the validator class, provided out of the box.
export class InputDirective implements Validator {
readonly validator = input<string>();
// for example:
readonly maxLength = input<string>();
// this injection is quite powerful, to target the content of the directive as Angular DOM
private el = inject(ElementRef);
validate(control: AbstractControl): ValidationErrors | null {
// ... for example
const maxlength = this.maxlength();
if (maxlength && control.value) {
if (Validators.maxLength(maxlength)(control)) {
this.errorText.set('Too long');
}
}
}
}Then in form, the following will be validated on input change:
<--olinput is the directive selector -->
<input type="text"
olinput
[formControlName]="something"
[required]="true"
[maxLength]="30"
/>Read about the full implementation in Taming Angular Forms.
In Svelte however, deciding which validation rule overrides the other, targeting the HTML input inside the component, and requesting the form validation upon demand, were pain points. The simplicity of code created in Svelte is not worth the trouble.
anchorBindable values
That being said, creating an immediate state and binding it to any input form, is much easier in Svelte. If you do not wish to control validation or have a simple form, it is useless boilerplate in Angular.
Updating a form input inside a component in Angular is a nightmare that never gets easier. The easiest way is to deal with the component that has a field, is via input and output signals. In Svelte however, all you need is a bindable property.
<form onsubmit={saveForm} novalidate>
<div class="spaced">
<Otp bind:value={formState.code} />
</div>
<button type="submit" class="btn-rev">SAVE</button>
</form>Where the Otp component simply looks like this, and that's it. The internal input value is reflected to the external formState.code.
<script>
let { value = $bindable() }: { value?: any } = $props();
</script>
<input
type="text"
bind:value={value}/>This brings us to the concept of directives. In Svelte, you can call a common javascript function anywhere in the template. You can also attach a behavior fairly easily. But when you need to deal with the outside world or have more accurate life cycle hooks for those attachments, there is nothing that can help. Here is an example:
// lib/lazy.ts
// example svelte attachment
export function lazy(url: string): Attachment {
return (element: HTMLImageElement) => {
if (!element) return;
if (!browser || !IntersectionObserver) {
element.src = url;
return;
}
// do some intersection observation ...
return () => {
// stop watching and destroy
};
};
}A directive in Angular is similar:
@Directive({
// choose the css selector
selector: '[olLazy]'
})
// dependecy injection for a safe version of the DOM element
constructor(
private el: ElementRef,
private renderer: Renderer2,
) { }
})
export class LazyDirective implements AfterViewInit, OnChanges, OnDestroy {
ngAfterViewInit() {
// life cycle hook for first time loading
}
ngOnChanges(c: SimpleChanges) {
// watch changes
}
ngOnDestroy(): void {
// destroy
}
}Looks like Angular has much more boilerplate for simple behavior. Wait until you need to detect the server platform:
// inject the actual request from server
private request = inject(REQUEST, { optional: true }) as Request;
// use it in a directive
this.request.get('user-agent')In Svelte, there is no way to find out the user agent in the context of an attachment. Go ahead, try, then let me know.
anchorRouting
Angular code driven routing benefits over Svelte choice of 1995 style of file-system based routing, hands down, wins my support. Common guys, WordPress friendly routing must have left some mark on your skin!
Creating logic rules embedded in a file name, that can be renames, deleted, or moved, to make room for more rules, is distasteful.
I am going to stop badgering Svelte(kit) now.
Almost everything that can be done in Angular routing, can somehow be reproduced using file-system based routing. On top of that, Angular also provides a Route class that supports the following:
anchorReuse strategy
Reuse strategy: allows a custom behavior of the route revisited in sequence, it can be used to store data as user reloads the same route with different parameters, or—the more popular need—destroy the route component and rebuild it.
In Svelte, a #key directive in templates destroys that part of the page when the key changes. But it's limited to one key, and it does not come with other less popular features of the Reuse Strategy. There is also the more vague invalidateAll option for the route function. I used it, and I must admit, I still don’t know what to expect.
<!--in svelte component -->
{#key id}
component content here, reload if id changed in same route
{/key}In Angular, besides the usual async pipes that observe route changes, a higher order reuse strategy can destroy the whole route and recreate it:
// in route reuse class
@Injectable({ providedIn: 'root' })
export class RouteReuseService extends RouteReuseStrategy {
// ...
shouldReuseRoute(curr: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot): boolean {
// example of forcing a route regeneration by route data
// read route data to figure out what to reuse, the pages that are troublesome should not reuse
// if data contains "resuse: never" make a check on params values, if changed, return false
if (future.routeConfig === curr.routeConfig) {
if (future.data && future.data['reuse'] === 'never') {
// check all params
let reuse = true;
curr.data['params'].forEach((n: string) => {
// fail if not exact match
reuse = reuse && curr.paramMap.get(n) === future.paramMap.get(n);
});
return reuse;
}
}
// reuse component? or don't reuse it?
return future.routeConfig === curr.routeConfig;
}
}
// provide it at boostrap
bootstrapApplication(AppComponent, {
providers: [
{ provide: RouteReuseStrategy, useClass: RouteReuseService }
]
});
// then use in route:
{
path: ':id',
component: OrderDetailsComponent,
data: { reuse: 'never', params: ['id'] },
}More boilerplate? yes. More control? You bet.
anchorPreloading
In Svelte there is only one data-attribute added in app.html to preload the code, it is not fine tuned, and unpredictable.
In Angular you can implement a service to choose which routes you need to be preloaded, and optionally fine tune it to preload after a delay.
<!-- sveltekit app.html -->
<body data-sveltekit-preload-code="eager">
</body>Here is an example of a preload strategy I have always used, to preload modules after a delay, note that the route in current context, does not preload, it simply immediately loads.
// preload strategy class that optionally delays preloaded routes by 5 seconds
export class PreloadService implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data?.['preload']) {
if (route.data['delay']) {
return timer(5000).pipe(mergeMap(() => load()), catchError(() => of(null)));
} else {
return load();
}
} else {
return of(null);
}
}
}
// then use per route:
{
path: 'shippers',
component: ShippersComponent,
loadChildren: () =>
// lazy load the route
import('./routes/shipper.route').then((m) => m.ShipperRoutes),
// preload anytime, after 5 seconds
data: { preload: true, delay: true },
},anchorNavigation events
In Svelte, besides the server handler, the +page.ts load events (which are not guaranteed to fire in sequence), onMount and onDestroy for every page, and component, there is also returned destroyers for $effect, and attach. You can watch navigation changes on root +layout.svelte, the object it returns is basic. In Angular, navigation events are observables that can be tapped onto, in root level (before any navigation occurs) and it returns richer information about the route. All components, and directives have onInit and onDestroy. Canceled events can be caught too, and this is where fine tuning error handling shines.
// watch navigation events on +layout.svelte
beforeNavigate((navigation) => {
const _url = navigation?.to?.url;
// save url to navigate to, then navigate
// ...
});In Angular, the routing events are "provided" on bootstrap, like everything in Angular
// of of the providers array:
provideEnvironmentInitializer () => {
const router: Router = inject(Router);
router.events
.pipe(
filter(
(e) =>
// optionally choose which events to track
// there are a lot more events available for tracking
e instanceof NavigationEnd ||
e instanceof NavigationCancel ||
e instanceof RouteConfigLoadEnd
)
)
.subscribe((event) => {
if (event instanceof NavigationEnd) {
// the event objects holds alot of information
if (event.urlAfterRedirects === '/404') {
}
const _url = event.url;
// ...
} else if (event instanceof NavigationCancel) {
// this happens when user isn't logged in, the navigation is canceled
// via the auth guard
// ...
}
});
};anchorReroute Hook
This is specific to Svelte. What it does is reroute according to any rule you feed it. I have used that for multi lingual interfaces so that the language is never part of the route system.
// it must be in a file named hook.ts, on root
export const reroute: Reroute = ({ url }) => {
// catch /en or /ar and remove it to a plain route
return reroutePath(url.pathname);
};In Angular there is nothing that I would do for language picker in development, since Angular dev environment has always been built around a single language, where the other languages are produced on build time. But the closest to reroute would probably be UrlSerializer.
Running all languages in development was a blessing in Svelte, until I had to maintain the language on the server, it started turning dark!
Complicated? One look at Route documentation in Angular still intimidates me. I have used only a handful of features, but nevertheless, they are features that proved to be hard to reproduce in Svelte. So simplicity is always appreciated, until you have a complicated situation.
Having free-from-file-system routing, to me, has always won.
- It's liberty to organize your project according to functionality, not route.
- Allows you to delay decision of routes, while building the business
- It makes it easy to reuse components, with minimal conditionals
- It centralizes all route-related issues in an explicit manner
And let's not forget, you don't have to rename files to fix a route issue! That is horrific 😱
anchorProjection
In Svelte you are provided with the snippet, which allows you to pass back the container element to process it in the wrapper. But there is no way for the component to know what this snippet has, unless you add enough boilerplate to wait for the snippet to load. There is no hook to tell you that the snippet content is ready.
// in html we render the snippet
{@render input(el => (element = el))}
// in script, find the object
const element = document.getElementById(id);
// to wait for the content to load, you need to bind:this of the snippet content, then pass it back to component, and watch
let element = $state(null);
$effect(() => {
if (element) {
// the content is loaded with the exact component inside snippet
}
});In HTML:
<!-- then you need to bind that whenever you use it -->
{#snippet input(ref)}
<div bind:this={ref} class="loaded-box></div>
{/snippet}This becomes tedious to repeat on every component. $effect too is not an ideal way to watch changes. But it does the job.
In Angular, the boilerplate of the component is inherently defined and bound by the wrapper component, or the directive defined. You can inject the content of the projected content, as a directive or as a component, and wait for the initialization, or hook to some function, or even call a function in the snippet (content island) component. It has the same challenge of choosing the right hook (Angular introduced two extra hooks recently to handle those edge cases), but besides that, the code of Angular does look more complicated. So for simple things it looks like you need to define a bit too many things to get projection working.
// you first use this
template:`<ng-content></ng-content>`;
// selector: ol-input
// then you inject
readonly inputDirective = contentChild(InputDirective);
// then you watch, in constructor using $effect
constructor() {
effect(() => {
// this targtes HTML element inside the ng-content
// assume the selector is ol-directive
const element = this.inputDirective()?.element;
// ...
});
}
}To use this, it's as simple as adding the directive to the target component projected content.
<ol-input>
this content is projected, with one specific element with a directive that can be singled out and targeted:
<input ... ol-directive />
</ol-inpit>I have run into many scenarios when I longed for the complexity of Angular.
To conclude: Svelte indeed has less colors, and sometimes that is an art of its own. But if you want to paint a Monalisa, you need to start mixing those colors manually.
Speaking of life cycle hooks, one more fine Tuesday is coming your way in this series. In addition to overview of SSR, bootstrapping and some badgering. 😴
What about you? Did Angular make you lazier?