What I use is a component which handles those calls and turns errors into toasts:
ToastErrorHandler.cmp:
<aura:component >
<aura:method name="handleResponse" action="{!c.handleResponse}">
<aura:attribute name="response" type="Object" />
<aura:attribute name="successHandler" type="Object" />
<aura:attribute name="errorHandler" type="Object" />
</aura:method>
</aura:component>
ToastErrorHandlerController.js
({
handleResponse : function(component, event) {
var params = event.getParam('arguments');
var response = params.response;
var successHandler = params.successHandler;
var errorHandler = params.errorHandler;
var state = response.getState();
if(state == 'SUCCESS') {
successHandler(response);
} else {
var toastEvent = $A.get("e.force:showToast");
var message = '';
if (state === "INCOMPLETE") {
message = 'Server could not be reached. Check your internet connection.';
} else if (state === "ERROR") {
var errors = response.getError();
if (errors) {
for(var i=0; i < errors.length; i++) {
for(var j=0; errors[i].pageErrors && j < errors[i].pageErrors.length; j++) {
message += (message.length > 0 ? '\n' : '') + errors[i].pageErrors[j].message;
}
if(errors[i].fieldErrors) {
for(var fieldError in errors[i].fieldErrors) {
var thisFieldError = errors[i].fieldErrors[fieldError];
for(var j=0; j < thisFieldError.length; j++) {
message += (message.length > 0 ? '\n' : '') + thisFieldError[j].message;
}
}
}
if(errors[i].message) {
message += (message.length > 0 ? '\n' : '') + errors[i].message;
}
}
} else {
message += (message.length > 0 ? '\n' : '') + 'Unknown error';
}
}
toastEvent.setParams({
title: 'Error',
type: 'error',
message: message
});
toastEvent.fire();
if(errorHandler) {
errorHandler(response);
}
}
}
})
In a component where I want to use it, I just include the <c:ToastErrorHandler aura:id="toastErrorHandler" />
in the component. And then instead of directly writing a handler for the callback of an action, I call the method from ToastErrorHandler:
var saveValuesAction = component.get("c.saveValues");
saveValuesAction.setParams({
'formDescriptionId' : component.get('v.formDescriptionId'),
'values' : JSON.stringify(values)
});
var toastErrorHandler = component.find('toastErrorHandler');
saveValuesAction.setCallback(this, function(response){
toastErrorHandler.handleResponse(
response,
function(response){
var toastEvent = $A.get("e.force:showToast");
toastEvent.setParams({
"title": $A.get("$Label.c.Form_Save_Success_Title"),
"message": $A.get("$Label.c.Form_Save_Success_Message"),
"type": "success"
});
toastEvent.fire();
})
});
$A.enqueueAction(saveValuesAction);
This allows me to share consistent error handling around all of my server-calls. It's probably not the perfect solution yet, but I think something like this works pretty well.