I have following code (it's simplified for illustration purposes). I'm creating records in different DB tables in proc1, proc2, and proc3. What I'm trying to achieve is...if I encounter an error while looping through temp-tables at any point (even after I created a bunch of DB records already), I want to roll everything back so no records are created. It catches errors if proc1, proc2, and proc3 with no issues but I cannot figure out how to pass those errors to the main processing block so it understands it and rolls everything back. In other words, the message ('error # main trans block') never pops up so the already created records stay in the DB. As a matter of fact, nothing gets rolled back.
DO TRANSACTION ON ERROR UNDO, THROW:
FOR EACH tt1:
RUN proc1.
FOR EACH tt2 WHERE tt2.field1 EQ tt1.field1:
RUN proc2.
FOR EACH tt3 WHERE tt3.field2 EQ tt2.field2:
RUN proc3.
END.
END.
END.
CATCH e AS PROGRESS.Lang.AppERROR:
MESSAGE 'error # main trans block'
VIEW-AS ALERT-BOX INFO BUTTONS OK.
END CATCH.
END.
PROCEDURE proc1.
DO TRANSACTION ON ERROR UNDO, THROW:
/* creating some DB records */
CATCH e AS PROGRESS.Lang.ERROR:
RETURN ERROR 'Proc1 ' + e:getmessage(1).
END CATCH.
END.
END PROCEDURE.
PROCEDURE proc2.
DO TRANSACTION ON ERROR UNDO, THROW:
/* creating some DB records */
CATCH e AS PROGRESS.Lang.ERROR:
RETURN ERROR 'Proc2 ' + e:getmessage(1).
END CATCH.
END.
END PROCEDURE.
PROCEDURE proc3.
DO TRANSACTION ON ERROR UNDO, THROW:
/* creating some DB records */
CATCH e AS PROGRESS.Lang.ERROR:
RETURN ERROR 'Proc3 ' + e:getmessage(1).
END CATCH.
END.
END PROCEDURE.
TIA
There are a couple of potential issues.
First, your temp-tables tt1 and tt2 need to be defined without the NO-UNDO flag.
Second, the FOR EACH blocks are using their default error handling behavior, which is ON ERROR UNDO, NEXT. So errors raised within the FOR EACH blocks will cause the current iteration to be undone, not the whole transaction.
I recommend adding the
BLOCK-LEVEL ON ERROR UNDO, THROW .
to the top of the program. Or at least
ROUTINE-LEVEL ON ERROR UNDO, THROW .
in combination with an ON ERROR UNDO, THROW option on all the FOR EACH blocks.
The BLOCK-LEVEL error handling option is available since OpenEdge 11.3 (or so).
Related
I have observed that in both cases, statements under these two blocks will execute the same. I do not understand what the difference is. Please can you explain.
Not surprisingly ON ERROR has to do with error handling. You should read up on this in the online help/manual since there's lots of ways on what to do.
DO is basically just a block. Without anything else it really doesn't do a lot. Paired with statements like TRANSACTION or ON ERROR it can greatly change how your program executes. You should check out the NO-ERROR statement as well. It also effects error handling.
In the below examples I force an error by trying to cast the string HELLO to an integer, this doesn't work of course.
DO ON ERROR, RETRY
This will repeat the block if there's an error and setting RETRY to true. If you don't LEAVE in the RETRY-block you will have a loop.
DO ON ERROR UNDO, RETRY:
IF RETRY THEN DO:
DISPLAY "RETRY".
/* Do some cleanup or what else */
LEAVE.
END.
i = INTEGER("HELLO").
END.
DO ON ERROR, THROW
A perhaps more modern approach when THROW - CATCH is used. Note that this also supresses the error from appearing (a bit like NO-ERROR).
DEFINE VARIABLE i AS INTEGER NO-UNDO.
DO ON ERROR UNDO, THROW:
i = INTEGER("HELLO").
END.
CATCH error AS Progress.Lang.Error :
MESSAGE "We had an error".
END CATCH.
DO:
The program will just halt on error
DEFINE VARIABLE i AS INTEGER NO-UNDO.
DO:
i = INTEGER("HELLO").
END.
The ON ERROR statement gives you control on what happens when the block fails. If you are using ROUTINE-LEVEL error handling for example, errors at the block level are not caught by default, so you can
DO ON ERROR UNDO,THROW:
END.
This will make sure the error is trapped. If you are using BLOCK-LEVEL error handling then this would be trapped by default.
This is just an example, and there are many things you can use ON ERROR for. Have a look at this documentation: https://help.consultingwerkcloud.com/openedge/117/rfi1424919692411.html
I am getting invalid cursor exception while iterating for 2nd time.
LOAD_TABLE_REF_CURSOR(V_TARIFF_TABLE_ROWS); -- V_TARIFF_TABLE_ROWS is a sys_refcursor.
when I am doing iteration like below.
LOAD_TABLE_REF_CURSOR(V_TARIFF_TABLE_ROWS);
LOOP
FETCH V_TARIFF_TABLE_ROWS INTO TAK_ROW;
EXIT WHEN V_TARIFF_TABLE_ROWS%NOTFOUND;
END LOOP;
The control inside the loop is going for 1st time fine and throwing exception as invalid cursor exactly at FETCH statement for the 2nd time.
Could someone tell whats wrong with 2nd iteration.
invalid cursor error is produced when;
FETCH cursor before OPENING the cursor.
FETCH cursor after CLOSING the cursor.
CLOSE cursor before OPENING the cursor.
LOAD_TABLE_REF_CURSOR(V_TARIFF_TABLE_ROWS);
open V_TARIFF_TABLE_ROWS;
-- use V_TARIFF_TABLE_ROWS
close V_TARIFF_TABLE_ROWS;
I recently was calling a procedure that contained a rasierror in the code. The raiserror was in a try catch block. Also a BEGIN TRAN was in the same try catch block after the raiserror. The Catch block is designed to ROLLBACK the transaction if the error occurred in the transaction. The way it does this is to check the ##TRANCOUNT if it is greater that 0 I know that it had started a transaction and needs to ROLLBACK. When testing with tSQLt the ##TRANCOUNT is always >0 so if it ever hits the CATCH Block the ROLLBACK is executed and tSQLt fails (because tSQLt is running in a transaction). When I rasie an error and the CATCH block is run tSQLt always fails the test. I have no way to test for the correct handling of the raiserror. How would you create a test case that can potentially ROLLBACK a transaction?
As you mentioned, tSQLt runs every test in its own transaction. To keep track of what is going on is relies on that same transaction to be still open when the test finishes. SQL Server does not support nested transactions, so your procedure rolls back everything, including the status information the framework stored for the current test. At that point tSQLt can only assume that something really bad happened. It therefore marks the test as errored.
SQL Server itself discourages a rollback inside a procedure, by throwing an error if that procedure was called within an open transaction. For ways to deal with this situation and some additional info check out my blog post about how to rollback in procedures.
As I'm just reading up on tSQLt this was one of the first questions that came to mind when I've learned each test ran in a transactions. As some of my stored procedures do start transaction, some even use nested transactions, this can become challenging. What I've learned about nested transactions, if you apply the following rules you can keep your code clean of constant error checking and still handle errors gracefully.
Always use a TRY/CATCH block when opening a transactions
Always commit the transactions unless an error was raised
Always rollback the transaction when an error is raised unless ##TRANCOUNT = 0
Always reraise the error unless you're absolutely sure there was no transaction open at the start of the stored procedure.
Keeping those rules in mind here is an example of a proc implementation and test code to test it.
ALTER PROC testProc
#IshouldFail BIT
AS
BEGIN TRY
BEGIN TRAN
IF #IshouldFail = 1
RAISERROR('failure', 16, 1);
COMMIT
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK;
-- Do some exception handling
-- You'll need to reraise the error to prevent exceptions about inconsistent
-- ##TRANCOUNT before / after execution of the stored proc.
RAISERROR('failure', 16, 1);
END CATCH
GO
--EXEC tSQLt.NewTestClass 'tSQLt.experiments';
--GO
ALTER PROCEDURE [tSQLt.experiments].[test testProc nested transaction fails]
AS
BEGIN
--Assemble
DECLARE #CatchWasHit CHAR(1) = 'N';
--Act
BEGIN TRY
EXEC dbo.testProc 1
END TRY
BEGIN CATCH
IF ##TRANCOUNT = 0
BEGIN TRAN --reopen an transaction
SET #CatchWasHit = 'Y';
END CATCH
--Assert
EXEC tSQLt.AssertEqualsString #Expected = N'Y', #Actual = #CatchWasHit, #Message = N'Exception was expected'
END;
GO
ALTER PROCEDURE [tSQLt.experiments].[test testProc nested transaction succeeds]
AS
BEGIN
--Act
EXEC dbo.testProc 0
END;
GO
EXEC tSQLt.Run #TestName = N'tSQLt.experiments'
Better to use a BEGIN TRY block after BEGIN TRANSACTION. I did this when I had a similar problem. This is more logical, because in CATCH block I checked IF ##TRANCOUNT > 0 ROLLBACK. This condition doesn't need to be checked if another error is raised before BEGIN TRANSACTION. And in this case you can test your RAISERROR functionality.
+1 to both the above answers.
However, if you don't want to use TRY .. CATCH, please try the following code. The part between the lines ----- represents the test, and above and below that represents tSQLt, before and after it calls your test. As you can see, the transaction started by tSQLt before calling the test, is still in place, as it expects, whether or not the error occurs. ##TRANSCOUNT is still 1
You can comment out the RAISERROR to try it with and without the exception being raised.
SET NOCOUNT ON
BEGIN TRANSACTION -- DONE BY tSQLt
PRINT 'Inside tSQLt before calling the test: ##TRANCOUNT = ' + CONVERT (VARCHAR, ##TRANCOUNT)
---------------------------------
PRINT ' Start of test ---------------------------'
SAVE TRANSACTION Savepoint
PRINT ' Inside the test: ##TRANCOUNT = ' + CONVERT (VARCHAR, ##TRANCOUNT)
BEGIN TRANSACTION -- PART OF THE TEST
PRINT ' Transaction in the test: ##TRANCOUNT = ' + CONVERT (VARCHAR, ##TRANCOUNT)
RAISERROR ('A very nice error', 16, 0)
PRINT ' ##ERROR = ' + CONVERT(VARCHAR,##ERROR)
-- PART OF THE TEST - CLEAN-UP
IF ##ERROR <> 0 ROLLBACK TRANSACTION Savepoint -- Not all the way, just tothe save point
ELSE COMMIT TRANSACTION
PRINT ' About to finish the test: ##TRANCOUNT = ' + CONVERT (VARCHAR, ##TRANCOUNT)
PRINT ' End of test ---------------------------'
---------------------------------
ROLLBACK TRANSACTION -- DONE BY tSQLt
PRINT 'Inside tSQLt after finishing the test: ##TRANCOUNT = ' + CONVERT (VARCHAR, ##TRANCOUNT)
With acknowledgement to information and code at http://www.blackwasp.co.uk/SQLSavepoints.aspx
I am working on a "classic" ASP application with a SQL Server 2000 database.
We have a stored procedure (let's call it SP0) that calls other stored procedures (let's say SP0.1, SP0.2 ...) which themselves call another stored procedure called SPX.
All those procedures generate errors when something goes wrong using RAISERROR().
We want to be able to launch SP0 with a parameter #errorsInResultSet which will change its behaviour : instead of "re-raising" the errors as it does so far, each sub-procedure will log the errors in a temporary table #detectedProblems and return it at the end.
Adding errors to the temporary table is not a problem, but I can not figure out how to ignore the errors generated by the nested stored procedures.
I have done this so far :
EXEC #rc = [SP0.1] #errorsAsResultSet = #errorsAsResultSet
IF (0 <> ##ERROR) OR (0 <> #rc)
BEGIN
IF (#errorsAsResultSet <> 0x1)
BEGIN
RAISERROR('SP0.1: Error for table Tests in db %s.%s', 16, 1, ##SERVERNAME, #db)
END
GOTO FAILURE
END
This works fine, but it still generate errors from the lowest SPX, which prevent it from being executed by ADO in classic ASP.
How can I ignore the errors ?
If you're happy that the errors are being logged and it's safe to continue, you can use ON ERROR RESUME NEXT on the line before the SP call. This will prevent the page from throwing errors.
To turn back on errors later in the page, you can use ON ERROR GOTO 0
In the end, it looks like there is no way to "hide" messages generated by PRINT or RAISERROR statements in SP0.1, SP0.2 from the calling Stored Procedure, which means that the execution is always interpreted as "erroneous" by ASP.
In the end, I rewrote a new Stored Procedure with a special parameter to configure how to report errors.
I have an ASP.NET site (VB.NET) that I'm trying to clean up. When it was originally created it was written with no error handling, and I'm trying to add it in to improve the User Experience.
Try
If Not String.IsNullOrEmpty(strMfgName) And Not String.IsNullOrEmpty(strSortType) Then
If Integer.TryParse(Request.QueryString("CategoryID"), i) And String.IsNullOrEmpty(Request.QueryString("CategoryID"))
MyDataGrid.DataSource = ProductCategoryDB.GetMfgItems(strMfgName, strSortType, i)
Else
MyDataGrid.DataSource = ProductCategoryDB.GetMfgItems(strMfgName, strSortType)
End If
MyDataGrid.DataBind()
If CType(MyDataGrid.DataSource, DataSet).Tables("Data").Rows.Count > 0 Then
lblCatName.Text = CType(MyDataGrid.DataSource, DataSet).Tables("Data").Rows(0).Item("mfgName")
End If
If MyDataGrid.Items.Count < 2 Then
cboSortTypes.Visible = False
table_search.Visible = False
End If
If MyDataGrid.PageCount < 2 Then
MyDataGrid.PagerStyle.Visible = False
End If
Else
lblCatName.Text &= "<br /><span style=""fontf-size: 12px;"">There are no items for this manufacturer</span>"
MyDataGrid.Visible = False
table_search.Visible = False
End If
Catch
lblCatName.Text &= "<br /><span style=""font-size: 12px;"">There are no items for this manufacturer</span>"
MyDataGrid.Visible = False
table_search.Visible = False
End Try
Now, this is trying to avoid generating a 500 error by catching exceptions. There can be three items on the query string, but only two matter here. In my test environment and in Visual Studio when I run this site, it doesn't matter if that item is on the query string. In production, it does matter. If that third item isn't present (SubCategoryID) on the query string, then the "There are no items for this manufacturer" displays instead of the data from the database.
In the two different environments I am seeing two different code execution paths, despite the same URLs and the same code base.
The site is running on Server 2003 with IIS 6.
Thoughts?
EDIT:
In response to the answer below, I doubt it's a connection error (though I see what you're getting to), as when I add the SubCategoryID to the query string, the site works correctly (displaying data from the database).
Also, if please let me know if you have any suggestions for how to test this scenario, without deploying the code back to production (it's been rolled back).
I think you should try to print out the exception details in your catch block to see what the problem is. It could anything for example a connection error to your database.
The error could be anything, and you should definitely consider printing this out or logging it somewhere, rather than making the assumption that there's no data. You're also outputting the same error message to the UI for two different code paths, which makes things harder to debug, especially without knowing if an exception occurred, and if so, what it was.
Generally, it's also better not to have a catch for all exceptions in cases like this, especially without logging the error. Instead, you should catch specific exceptions and handle these appropriately, and any real exceptions can get passed up the stack, ideally to a global error handler which can log it and/or send out some kind of error notification.
I discovered the reason yesterday. In short it was because when I copied my files from my computer into my dev-test environment, I missed a file, which ironically caused it to work, rather than not. So in the end it would have functioned the same in both environments.