-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Either or throw? #13
Comments
TL;DRI'm not sure you are using Either reasons
It's not a matter of how simple is to read the actual value, or how handy is the stacktrace but a matter to be sure that errors are described and handled. From your examples: try {
final result = await myUseCase.call(MyUseCaseParams());
result.fold((failure) {
if (failure is SpecificFailure) {
// Handle my specific failure
return;
}
// Handle a failure
}, (myRes) async {
// Hendle result
})
} catch (error) {
// Recupera la UI in caso fosse necessario
retrhow; // Invia l'errore al logger perchè non gestito
} This is not the right way to use an either. If somebody is returning you an either, is explicitly assuring you that the call will not throw. So this should just be final result = await myUseCase.call(MyUseCaseParams());
result.fold((failure) {
if (failure is SpecificFailure) {
// Handle my specific failure
return;
}
// Handle a failure
}, (myRes) async {
// Hendle result
}) On failure handling final result = await myUseCase.call(MyUseCaseParams());
if (result is Left<Failure, MyResultType>) {
final failure = result.value;
if (failure is SpecificFailure) {
// Handle my specific failure
return;
}
// Handle a failure
return;
}
final result = await myUseCase.call(MyUseCaseParams());
if (result is Left<Failure, MyResultType>) {
// Stesso codice della precedente gestione della failure
return;
}
// Handle all results This is not the way // somewhere..
void errorHandle(Failure error) {
if (failure is SpecificFailure) {
// Handle my specific failure
return;
}
// Handle a failure
return;
}
final result = await myUseCase.call(MyUseCaseParams());
result
.map(() => await myOtherUseCase.call(MyUseCaseParams()) // or flatMap or whatever...
.fold(errorHandler, (value) => {
// do something with the value
}) Moreover, Either should encourage you modelling and declaring your internal exceptions. On this: try {
final result = await myUseCase.call(MyUseCaseParams());
} on Failure {
// Handle failure
} catch (error) {
// Recupera la UI in caso fosse necessario
retrhow; // Invia l'errore al logger perchè non gestito
} Nobody ensures you that somebody will use your api ( // Possible reasons
// I'm a lazy person that does not read your documented throws
// I'm a noobie and nobody told me that if I don't try/catch this block the world will end
final result = await myUseCase.call(MyUseCaseParams()); Basically, you are making examples of wrong usages.
In the end, if you don't feel comfortable using this construct as "final consumer" of the resulting apis it is not a problem, I just want to be sure that you get the right reason behind the usage of Using
If you don't need these behaviours because your are handling exceptions somewhere else in your code, |
@kuamanet Non mi è chiaro l'intento che vuoi promuovere con questo commento. E' chiaro l'intento di PS: Either non permette di conoscere la lista degli errori che riceverai. Come il throw. Deve essere scritto nella documentazione |
Non mi è chiara questa risposta. La discussione dovrebbe vertere su quali dei due approcci sono meglio su dart se Mi hai spiegato cosa fa e come si usa e come dovrebbe essere usato |
Basarsi sullo stack trace non e' certamente una best practice. Ma soprattutto, l'utilizzatore di either non deve avere interesse allo stacktrace, deve solo gestire come meglio crede i diversi tipi di errori.
Non e' vero. Dipende da come lo scrivi. class BaseError {}
class Error1 extends BaseError {}
class Error2 extends BaseError {}
Either<BaseError, String> either; // so esattamente l'elenco di possibili errori
try {
final result = await myUseCase.call(MyUseCaseParams());
} on Failure {
// Handle failure
} catch (error) {
// Recupera la UI in caso fosse necessario
retrhow; // Invia l'errore al logger perchè non gestito
}
// vs
final result = await myUseCase.call(MyUseCaseParams());
result.fold(errorHandler, successHandler)
Di preciso dov'e' l'aumento di complessita'? Either non e' in alternativa al throw. Either serve
connectToBleDevice.call().fold(onConnectionError, onDeviceConnected)
....
void onConnectionError(ConnectionFailure f) {// so esattamente quali possono essere le failures, non serve che vado a leggermi cosa fa la usecase. Chi l'ha scritta ha descritto in ConnectionFailure (e sue sottoclassi) i vari motivi di fallimento dell'azione
if(f is MissingBluetoothPermission) {
....
} else ...
}
In particolare try {
final result = await myUseCase.call(MyUseCaseParams());
} on Failure {
// Handle failure
} catch (error) {
// Recupera la UI in caso fosse necessario
retrhow; // Invia l'errore al logger perchè non gestito
} Questo sembra piu' codice che dovrebbe stare dentro una usecase Il quesito che poni (Either or throw) e' sbagliato alla base. Non sono due modi alternativi di fare la stessa cosa. |
A me sembra che stai spostando quello che dovrebbe essere dentro ad una usecase in altri posti. Le usecase non sono solo un bridge 1 a 1 tra il data layer e il presentation layer. La domanda al massimo puo' essere "Come vogliamo gestire gli errori?"
Io tendenzialmente preferirei la 2. |
Si vero. Ma siamo anche sviluppatori di quella libreria e ci interessa conosce da dove proviene una failure in caso di debug.
Questo lo puoi fare anche throw. Non è un valido argomento Allora l'implementazione attuale delle use case è sbagliata. Dovresti avere una Failure base per ogni UseCase
Questo dovrebbe essere try {
final result = await myUseCase.call(MyUseCaseParams());
} on Failure {
// Handle failure
} catch (error) {
// Recupera la UI in caso fosse necessario
retrhow; // Invia l'errore al logger perchè non gestito
}
// vs
void errorHandler(failure) {
// Handle failure
}
void successHandler(result) {
// ...
}
final result = await myUseCase.call(MyUseCaseParams());
result.fold(errorHandler, successHandler) Principalmente la complessità la si ritrova quando devi fare più either uno dentro l'altro, in successione
E' soggettivo. Dipende soltanto dal paradigma che siamo consoni da usare
Anche se usi Either potresti ottenere final either = ...;
either.fold((failure) {
// Non faccio nulla
}, (success) {...}) Quindi anche in questo caso non li stai gestendo |
Vediamo qualcosa di più complesso. Prova a scrivere questo con l'Either fn() async {
try {
final documentGroupFB = <int, GroupDocumentFieldBloc>{
KycFormSteps.idDocument: idDocumentFB,
KycFormSteps.proofOfAddress: proofOfAddressFB,
KycFormSteps.selfie: selfieFB,
}[state.currentStep]!;
final documentsFB = documentGroupFB.documentsFB;
final countryFB = documentGroupFB.countryFB;
// Upload any documents from this current step
for (final docFB in documentsFB.value) {
final file = docFB.value!;
final data = docFB.state.extraData!;
// Not upload already uploaded file
if (file.mimeType == 'fake') continue;
try {
await _uploadUserDocumentUC.call(UploadUserDocumentParams(
file: file,
type: data.type,
subType: data.subType,
country: countryFB.value,
));
} on Failure catch (failure, stackTrace) {
// File is big
if (failure is RequestTooLargeFailure) {
docFB.addFieldError('Request to large', isPermanent: true);
emitFailure();
lg.w({
'${state.currentStep} Failure on upload document ${data.type}.${data.subType}': {
'fileName': file.name,
'Size': await file.length()
}
}, failure, stackTrace);
return;
}
rethrow;
}
}
// If current step is a last step
// Mark product completed and open a wallet
if (state.currentStep == KycFormSteps.selfie) {
await _acquireKycProductUC.call(ProductId.account);
}
emitSuccess();
} on Failure catch (failure) {
emitFailure(failureResponse: failure);
}
} |
Sinceramente io non lo so scrivere con Either senza usare |
ci interessa internamente, non all'esterno. Perche' dovremmo trasformare un null pointer exception in failure? null pointer exception e' un bug da risolvere, non un'eccezione da gestire e presentare all'esterno
Sicuramente avere set di usecase che non hanno failure con classi basi comuni aiuterebbe la gestione dell'errore
usecase == businesslogic, non e' proprio super soggettivo
final either = ...; Sí, posso non voler gestire la failure. Ma scritto cosí é esplicito either.fold(noop, onSucces) Scritto cosí non sappiamo se veramente non si voleva gestire l'errore o se ci si e' dimenticati /// throws Exception1
/// throws Exception2
/// throws ...
void errorProneMethod() {... }
try {
errorProneMethod()
} on Exception1 {
...
} fn() async {
final documentGroupFB = <int, GroupDocumentFieldBloc>{
KycFormSteps.idDocument: idDocumentFB,
KycFormSteps.proofOfAddress: proofOfAddressFB,
KycFormSteps.selfie: selfieFB,
}[state.currentStep]!;
final documentsFB = documentGroupFB.documentsFB;
final countryFB = documentGroupFB.countryFB;
// Upload any documents from this current step
for (final docFB in documentsFB.value) {
final file = docFB.value!;
final data = docFB.state.extraData!;
// Not upload already uploaded file
// if (file.mimeType == 'fake') continue; THIS SHOULD BE INSIDE THE USECASE
final res = await _uploadUserDocumentUC.call(UploadUserDocumentParams(
file: file,
type: data.type,
subType: data.subType,
country: countryFB.value,
));
res.map(onUploadSuccess).leftMap(onUploadError(docFB));
// or res.fold(onUploadError(docFB), onUploadSuccess)
if (res.isLeft()) {
return; //stop uploading at first error
}
}
}
onUploadSuccess(DocumentEntity r) async {
// If current step is a last step
// Mark product completed and open a wallet
if (state.currentStep == KycFormSteps.selfie) {
await _onSubmittingLastStep();
} else {
emitSuccess();
}
}
Function(Failure) onUploadError(InputFieldBloc<XFile, DocumentFieldBlocData> docFB) {
return (Failure failure) {
// ALSO LOGGING SHOULD BE INSIDE THE USECASE LAYER
/*lg.w({
'${state.currentStep} Failure on upload document ${data.type}.${data.subType}': {
'fileName': file.name,
'Size': await file.length()
}
}, res.left);*/
// File is big
switch (failure.runtimeType) {
case RequestTooLargeFailure:
docFB.addFieldError('Request to large', isPermanent: true);
emitFailure();
break;
default:
emitFailure(failureResponse: failure);
}
};
}
|
Non fa le stesse cose che ho scritto nel mio codice.
A parte che è bruttissimo scritto cosi Scusa ti aggiungo void onSubmittingLastStep() {
try {
await _acquireKycProductUC.call(ProductId.account);
emitSuccess();
} on Failure catch (failure) {
emitFailure(failureResponse: failure);
}
}
|
non ho capito scusami.. la parte commentata e' il logging... che c'entra il futuro? |
Per loggare ha bisogno di un valore ritornato da |
Non capisco perche' continui a mettere le chiamate alle usecase in un try catch |
Devo riscrivere il messaggio precedente? che per sbaglio lo ho cancellato |
Esempio di use case con try e catch e la failure |
le call / tryCall sono gia' async, cosa ti disturba? try {
final doc = await _storageRepo.upload(
file,
CreateDocumentEntity((b) => b
..type = params.type
..subType = params.subType
..country = params.country?.code),
);
} on Error {
lg.w(
{
'${params.currentStep} Failure on upload document ${params.type}.${params.subType}': {
'fileName': file.name,
'Size': await file.length()
}
},
);
} |
Se la chiamata ha prodotto un null pointer exception non ha senso riprovarci. Riprodurra' un null pointer exception. Tra le varie avrai pure lo stack trace 😅 |
Per favore riprendi lo stesso codice che ho scritto prima e capirai dove sta il problema senza cambiarne il funzionamento fn() async {
try {
final documentGroupFB = <int, GroupDocumentFieldBloc>{
KycFormSteps.idDocument: idDocumentFB,
KycFormSteps.proofOfAddress: proofOfAddressFB,
KycFormSteps.selfie: selfieFB,
}[state.currentStep]!;
final documentsFB = documentGroupFB.documentsFB;
final countryFB = documentGroupFB.countryFB;
// Upload any documents from this current step
for (final docFB in documentsFB.value) {
final file = docFB.value!;
final data = docFB.state.extraData!;
// Not upload already uploaded file
if (file.mimeType == 'fake') continue;
try {
await _uploadUserDocumentUC.call(UploadUserDocumentParams(
file: file,
type: data.type,
subType: data.subType,
country: countryFB.value,
));
} on Failure catch (failure, stackTrace) {
// File is big
if (failure is RequestTooLargeFailure) {
docFB.addFieldError('Request to large', isPermanent: true);
emitFailure();
lg.w({
'${state.currentStep} Failure on upload document ${data.type}.${data.subType}': {
'fileName': file.name,
'Size': await file.length()
}
}, failure, stackTrace);
return;
}
rethrow;
}
}
// If current step is a last step
// Mark product completed and open a wallet
if (state.currentStep == KycFormSteps.selfie) {
await _acquireKycProductUC.call(ProductId.account);
}
emitSuccess();
} on Failure catch (failure) {
emitFailure(failureResponse: failure);
}
} |
Basically the behaviour is
right? fn() async {
final documentGroupFB = <int, GroupDocumentFieldBloc>{
KycFormSteps.idDocument: idDocumentFB,
KycFormSteps.proofOfAddress: proofOfAddressFB,
KycFormSteps.selfie: selfieFB,
}[state.currentStep]!;
final documentsFB = documentGroupFB.documentsFB;
final countryFB = documentGroupFB.countryFB;
// Upload any documents from this current step
var somethingFailed = false;
for (final docFB in documentsFB.value) {
final res = await _uploadUserDocumentUC.call(fieldBlocToParams(docFB));
res.leftMap(onUploadError(docFB));
somethingFailed = res.isLeft();
if (somethingFailed) {
break;
}
}
if (!somethingFailed && state.currentStep == KycFormSteps.selfie) {
final res = await _acquireKycProductUC.call(ProductId.account);
res.leftMap(...) // possibly handle this uc failures
somethingFailed = res.isLeft();
}
if (!somethingFailed) {
emitSuccess();
}
}
Function(Failure) onUploadError(InputFieldBloc<XFile, DocumentFieldBlocData> docFB) {
return (Failure failure) async {
switch (failure.runtimeType) {
case RequestTooLargeFailure:
docFB.addFieldError('Request to large', isPermanent: true);
emitFailure();
break;
default:
emitFailure(failureResponse: failure);
}
};
}
UploadUserDocumentParams fieldBlocToParams(InputFieldBloc<XFile, DocumentFieldBlocData> docFB) {
return UploadUserDocumentParams(
file: docFB.value!,
type: docFB.state.extraData!.type,
subType: docFB.state.extraData!.subType,
country: countryFB.value,
);
} |
Ripeto, hai saltato parti del codice |
Cioè hai gli either mi aspetterei che utilizzassi quelli per controllare se ha fallito o no, non avere secondi variabili. Questo è molto "magico", non vorrei mai dover scrivere questo, inoltre non puoi scriverlo questo pezzo di codice Function(Failure) onUploadError(InputFieldBloc<XFile, DocumentFieldBlocData> docFB) {
return (Failure failure) async {
switch (failure.runtimeType) {
case RequestTooLargeFailure:
docFB.addFieldError('Request to large', isPermanent: true);
emitFailure();
break;
default:
emitFailure(failureResponse: failure);
}
};
} |
|
Ma che parti?? Piu' complicato di un try catch nested con un rethrow?? Function(Failure) onUploadError(InputFieldBloc<XFile, DocumentFieldBlocData> docFB) {
return (Failure failure) async { quell'async e' inutile , si puo togliere, mi e' rimasto li |
tipo un |
si |
lg.w({
'${state.currentStep} Failure on upload document ${data.type}.${data.subType}': {
'fileName': file.name,
'Size': await file.length()
}
}, failure, stackTrace);
return; |
Non abbiamo detto che va nella usecase? |
Vorrei capire come dovrei gestire delle use case annidate, dato che ritornano futuri e Either non li supporta |
Probabilmente il casino è quello, dovremmo restituire un |
@SimoneBressan Con quelle e' possibile fare il flatMap/map/leftMap delle varie usecase. Li puoi usare con la consapevolezza che map su un either esegue solo se l'either e' un right, quindi nell'esempio di upload potresti mappare l'array di documenti e mettere un flatMap per eseguire qualcosa solo quando tutte le usecase vanno a buon fine e un leftMap all'errore. Domani provo a scrivertela bene. L'alternativa e' creare una usecase che aggrega piu' usecase. |
Non è un po troppo complicato? Sarebbe molto semplice se utilizzassimo try e catch |
XD se il problema è "è troppo complesso mi rallenta lo sviluppo" ok, ma allora stiamo discutendo del nulla da 2 giorni :D |
Io sto discutendo su: Ha senso usare Either in dart? |
Sarebbero bello che anche altre persone partecipassero alla discussione |
Il problema secondario ma non risolto perchè è un BrakingChange è in produzione, questo approccio alle use case basato sugli Either porta ad alcuni problemi:
Lanciare le Failure con throw porterebbe a notevoli vantaggi, le failure non gestite andrebbero direttamente sul logger e verebbe creato lo stackTrace solo quando il logger riceve l'errore inoltre i tipi sarabbero sicuri e la scrittura del codice verebbe piu breve e leggibile.
Either:
if ( is )
per ogni failureprovider
ma richiede una scrittura prolissa per il recupero del valorefinal res = context.read<Either<Failure, Success>>()
e poco usabile/usato lato presentationThrowCatch:
on
provider
non è possibile. E' veramente necessario? No, dovresti passare sempre per ilbloc
appena la logica diventa complicata la quale necessita l'utilizzo diprovider
Sono mostrati solo esempi sicuri da scrivere, che non possono presentare errori di tipo
Con Either:
Esempio 1:
Esempio 2:
Con throw e catch
Esempio 3:
Link esterni:
provider
bloc
Altri problemi conosciuti sono stati presentati in parte in questa PR #12.
Come dovresti far per recuperare la UI da un errore in aspettato con i diversi casi di utilizzo?
Con Either: Esempio 1:
Con throw e catch: Esempio 2:
Non è molto chiaro mischiare i due casi di gestione dell'errore per quanto riguarda l'esempio 1 invece nell'esempio 2 rimane tutto molto chiaro
The text was updated successfully, but these errors were encountered: